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 have already solved" idea, spread across two axes instead of one. dp[i][j] depends on neighboring cells in a grid, not only the one or two entries before it in a line. Nothing conceptually new happens here. The table only grew a second dimension.

Imagine that you fill in a grid of answers cell by cell ("how many ways to get here" or "what is the cheapest path to here"), where each cell needs only 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, and left to right in each row. By the time you reach any cell, everything that it depends on is already there and waiting.

The exact same discipline from DP Fundamentals applies here first: say what dp[i][j] means, in a full sentence, before you write the recurrence. "dp[i][j] is the number of ways to reach cell (i, j) from the top-left corner" is a sentence that 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

    # The base case row and 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 "the number of distinct paths from (0,0) to (i,j)". The first row and the first column are each forced into exactly one path. There is no 'above' to come from in row 0, and no 'left' to come from in column 0. This is why they are seeded by hand instead of from 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 shape of the recurrence (this cell depends on the cell above and the cell to the left) is identical. Only the combine step changed, from + to min. To recognize that shape is the actual transferable skill, not to memorize 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] reads only 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 (walk left to right, because dp[i][j-1] must already be the NEW value), even a single row. This is a direct callback to the rolling-variable trick from DP Fundamentals, one dimension up.
Know It

Every cell in the grid is visited exactly one time and does O(1) work per cell. So time is O(rows Γ— cols) for all of these. There is no way around a touch of 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), because 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] is computed per cell, each in O(1) from already-filled neighbors.
Unique Paths / Minimum Path Sum β€” rolling rowO(rows Γ— cols)O(cols)The same number of cells is computed, but only the current row and the previous row need to exist in memory at the same time. 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 with the general two-neighbor recurrence

Row 0 and column 0 do not have both neighbors that 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 "there is exactly one way in" or "carry the running total forward"), and you must apply that rule by hand before the main double loop starts. To reuse the general formula there reads an out-of-bounds cell, or quietly treats a missing neighbor as zero. That is wrong for the count in Unique Paths, and often wrong for the running total in Minimum Path Sum too.

Iterating in the wrong order

a cell is read before it is computed

dp[i][j] = dp[i-1][j] + dp[i][j-1] assumes that the cell above and the cell to the left are already filled in. This is guaranteed only if the loops go top-to-bottom and left-to-right. If you iterate bottom-to-top or right-to-left by mistake (easy to do if the recurrence is adapted from a different problem without a check of direction), dp[i][j] reads a cell that is still its default or uninitialized value instead of a real answer.

Forgetting an obstacle or blocked-cell case

a cell is impassable and needs to pass 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, because no paths reach it, before the general recurrence ever runs. If you skip that check and let the normal dp[i-1][j] + dp[i][j-1] formula apply to a blocked cell, you count paths that walk straight through a wall.

The rolling-row optimization overwriting a value still needed

you update a row in place, left to right, but read a value that was already overwritten this pass

When you collapse 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) must still be the value of the last row at that same index. If you get the read/write order backward, the in-place update corrupts data that it has not reached yet. The same overwrite risk appears anywhere a DP table is collapsed into fewer rows or variables than states. The direction that you scan changes what is still "old" versus already "new".

Use It
Unique Paths
Use uniquePaths() from Build It, exactly.
Medium
Unique Paths II
This is Unique Paths plus an obstacle check: dp[i][j] = 0 immediately if that cell is blocked, before you apply the normal recurrence.
Medium
Minimum Path Sum
Use minPathSum() from Build It, exactly.
Medium
Triangle
This is 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, because every row needs only the row below it.
Medium
Maximal Square
dp[i][j] = the 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 that you see.
Medium
Minimum Falling Path Sum
This is like Triangle but on a full square grid that falls downward: dp[i][j] = matrix[i][j] + the min of the three cells diagonally or directly above it.
Medium
Out of Boundary Paths
dp[step][i][j] = the number of ways to push the ball out of bounds within the remaining moves. This is a grid DP with a third axis (moves used so far) on top.
Medium
Count Square Submatrices with All Ones
Use the same recurrence as Maximal Square, but instead of a track of the largest side length, sum every dp[i][j]. The dp value of each cell is exactly the count of squares that end there.
Medium