LearnAbout

DP on Grids (2D)

On this page
Quick reference
Unique Paths / Minimum Path Sum β€” full tableO(rows Γ— cols)
Unique Paths / Minimum Path Sum β€” rolling rowO(rows Γ— cols)
The Idea

DP Fundamentals built a table of answers along one axis: dp[i] depended on dp[i-1] and dp[i-2], a straight line of subproblems. Grid DP is the same "remember what you've already solved" idea spread across two axes instead of one β€” dp[i][j] depends on neighboring cells in a grid, not just the one or two entries before it in a line. Nothing conceptually new happens here; the table just grew a second dimension.

Picture filling in a grid of answers cell by cell β€” "how many ways to get here" or "what's the cheapest path to here" β€” where each cell only needs the answer already computed in the cell above it and the cell to its left. Fill the grid in reading order, top row to bottom row, left to right within each row, and by the time you reach any cell, everything it depends on is already sitting there waiting.

The exact same discipline from DP Fundamentals applies here first: say what dp[i][j] means, in a full sentence, before writing the recurrence. "dp[i][j] is the number of ways to reach cell (i, j) from the top-left corner" is a sentence you can check every line of code against.
Build It
A 2D table, filled in reading order
function solve(grid):
    rows, cols = grid dimensions
    dp = 2D array, rows x cols

    # base case row/column often follow a SPECIAL rule, not the general recurrence
    seed dp[0][*] and dp[*][0] by hand

    for i from 1 to rows - 1:
        for j from 1 to cols - 1:
            dp[i][j] = combine(dp[i - 1][j], dp[i][j - 1], grid[i][j])

    return dp[rows - 1][cols - 1]
Unique Paths β€” dp[i][j] = ways to reach (i, j) moving only right or down
function uniquePaths(rows, cols):
    dp = 2D array, rows x cols
    for i from 0 to rows - 1:
        dp[i][0] = 1              // only one way to reach any cell in column 0: straight down
    for j from 0 to cols - 1:
        dp[0][j] = 1              // only one way to reach any cell in row 0: straight right
    for i from 1 to rows - 1:
        for j from 1 to cols - 1:
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1]   // arrived from above, or arrived from the left
    return dp[rows - 1][cols - 1]
dp[i][j] means "number of distinct paths from (0,0) to (i,j)." The first row and first column are each forced into exactly one path β€” there's no 'above' to come from in row 0, no 'left' to come from in column 0 β€” which is why they get seeded by hand instead of falling out of the general recurrence.
Minimum Path Sum β€” same shape, min instead of sum
function minPathSum(grid):
    rows, cols = grid dimensions
    dp = 2D array, rows x cols
    dp[0][0] = grid[0][0]
    for j from 1 to cols - 1:
        dp[0][j] = dp[0][j - 1] + grid[0][j]     // row 0: only one way in, from the left
    for i from 1 to rows - 1:
        dp[i][0] = dp[i - 1][0] + grid[i][0]     // column 0: only one way in, from above
    for i from 1 to rows - 1:
        for j from 1 to cols - 1:
            dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i][j - 1])
    return dp[rows - 1][cols - 1]
Unique Paths counts how many ways; Minimum Path Sum asks which way is cheapest β€” the recurrence's shape (this cell depends on the cell above and the cell to the left) is identical, only the combine step changed from + to min. Recognizing that shape is the actual transferable skill, not memorizing two separate problems.
Space optimization β€” collapsing to two rows, or one
function minPathSum(grid):
    rows, cols = grid dimensions
    prevRow = array of size cols
    prevRow[0] = grid[0][0]
    for j from 1 to cols - 1:
        prevRow[j] = prevRow[j - 1] + grid[0][j]

    for i from 1 to rows - 1:
        currRow = array of size cols
        currRow[0] = prevRow[0] + grid[i][0]
        for j from 1 to cols - 1:
            currRow[j] = grid[i][j] + min(prevRow[j], currRow[j - 1])
        prevRow = currRow

    return prevRow[cols - 1]
dp[i][j] only ever reads row i-1 and the current row i β€” row i-2 and earlier are never touched again once row i-1 is done with them. So the full rows x cols table collapses to two 1D rows, or with careful in-place updates (walking left to right, since dp[i][j-1] must already be the NEW value), even a single row. Direct callback to DP Fundamentals' rolling-variable trick, one dimension up.
Know It

Every cell in the grid is visited exactly once and does O(1) work per cell, so time is O(rows Γ— cols) across the board β€” no way around touching every cell when every cell's answer genuinely depends on its neighbors. Space for a full table is O(rows Γ— cols); the rolling-row trick above drops that to O(cols), since only the immediately previous row is ever needed again.

OperationTimeSpaceWhy
Unique Paths / Minimum Path Sum β€” full tableO(rows Γ— cols)O(rows Γ— cols)One dp[i][j] computed per cell, each in O(1) from already-filled neighbors.
Unique Paths / Minimum Path Sum β€” rolling rowO(rows Γ— cols)O(cols)Same number of cells computed, but only the current and previous row need to exist in memory at once β€” everything above that is done being read from.
Break It

Getting the first row/column base case wrong

row 0 or column 0 is filled using the general two-neighbor recurrence

Row 0 and column 0 don't have both neighbors the general recurrence expects β€” row 0 has no row above it, column 0 has no column to its left. They follow their own, simpler rule (often just "there's exactly one way in" or "carry the running total forward"), and that rule has to be applied by hand before the main double loop starts. Reusing the general formula there reads an out-of-bounds cell or silently treats a missing neighbor as zero, which is wrong for Unique Paths' count and often wrong for Minimum Path Sum's running total too.

Iterating in the wrong order

a cell is read before it's been computed

dp[i][j] = dp[i-1][j] + dp[i][j-1] assumes the cell above and the cell to the left are already filled in β€” which is only guaranteed if the loops go top-to-bottom, left-to-right. Iterate bottom-to-top or right-to-left by mistake (easy to do if the recurrence gets adapted from a different problem without checking direction) and dp[i][j] reads a cell that's still its default/uninitialized value instead of a real answer.

Forgetting an obstacle or blocked-cell case

a cell is impassable and needs to propagate 0 forward, not the general formula

A grid variant with obstacles (like Unique Paths II) needs an explicit check: if this cell is blocked, dp[i][j] = 0 β€” no paths reach it, full stop β€” before the general recurrence ever runs. Skipping that check and letting the normal dp[i-1][j] + dp[i][j-1] formula apply to a blocked cell counts paths that walk straight through a wall.

The rolling-row optimization overwriting a value still needed

updating a row in place, left to right, but reading a value that was already overwritten this pass

When collapsing to a single row (not two), dp[j-1] must be the just-updated value for this row, but dp[j] (before you overwrite it) still needs to be last row's value at that same index β€” get the read/write order backwards and the in-place update corrupts data it hasn't gotten to yet. The same overwrite risk shows up anywhere a DP table is collapsed into fewer rows/variables than states: which direction you scan changes what's still "old" versus already "new."

Use It
Unique Paths
uniquePaths() from Build It, verbatim.
Medium
Unique Paths II
Unique Paths plus an obstacle check: dp[i][j] = 0 immediately if that cell is blocked, before applying the normal recurrence.
Medium
Minimum Path Sum
minPathSum() from Build It, verbatim.
Medium
Triangle
A jagged grid, not a rectangle β€” dp[i][j] = triangle[i][j] + min(dp[i+1][j], dp[i+1][j+1]) works cleanly bottom-up, since every row only needs the row below it.
Medium
Maximal Square
dp[i][j] = side length of the largest all-1s square with its bottom-right corner at (i,j) = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) when grid[i][j] is 1, else 0 β€” track the max dp value seen.
Medium
Minimum Falling Path Sum
Like Triangle but on a full square grid falling downward: dp[i][j] = matrix[i][j] + min of the three cells diagonally/directly above it.
Medium
Out of Boundary Paths
dp[step][i][j] = number of ways to push the ball out of bounds within the remaining moves β€” a grid DP with a third axis (moves used so far) layered on top.
Medium
Count Square Submatrices with All Ones
Same recurrence as Maximal Square, but instead of tracking the largest side length, sum every dp[i][j] β€” each cell's dp value is exactly the count of squares that end there.
Medium