Paths in a Grid with Obstacles
Given a grid where some cells are blocked obstacles, and starting at the top-left corner while moving only right or down at each step, count the distinct paths to the bottom-right corner that avoid every obstacle.
Do this lesson first: climbing stairsExample input
grid = [[0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 0, 1], [0, 1, 0, 0, 0], [1, 0, 0, 0, 0]], where 1 marks an obstacle
Expected output
17
Break it down
Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.
Fill the table
The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.
Press start. The animation stops at every cell YOUR recurrence must fill.
dp[0][0] = 1 is the only base, and that single difference from the unobstructed grid is worth dwelling on. There the whole first row and first column can be declared 1 up front; here they cannot, because one obstacle in row 0 makes every cell to its right unreachable, so the edges have to be computed like every other cell, just with one candidate instead of two. Filling row-major keeps the cell above and the cell to the left final before they are used. A blocked cell is the one write in this table that depends on nothing at all: it has no cells to consult, so the walkthrough does not stop and ask you to predict it, because there is no arithmetic there to predict.
- 1
function countPaths(grid) { - 2
const n = grid.length, m = grid[0].length; - 3
const dp = Array.from({ length: n }, () => new Array(m).fill(0)); - 4
dp[0][0] = 1; - 5
for (let r = 0; r < n; r++) { - 6
for (let c = 0; c < m; c++) { - 7
if (grid[r][c] === 1) { dp[r][c] = 0; continue; } - 8
if (r === 0 && c === 0) continue; - 9
const above = r > 0 ? dp[r - 1][c] : 0; - 10
const left = c > 0 ? dp[r][c - 1] : 0; - 11
dp[r][c] = above + left; - 12
} - 13
} - 14
return dp[n - 1][m - 1]; - 15
}
The code, the trap, the variations
- 1
function countPaths(grid) { - 2
const n = grid.length, m = grid[0].length; - 3
const dp = Array.from({ length: n }, () => new Array(m).fill(0)); - 4
dp[0][0] = 1; - 5
for (let r = 0; r < n; r++) { - 6
for (let c = 0; c < m; c++) { - 7
if (grid[r][c] === 1) { dp[r][c] = 0; continue; } - 8
if (r === 0 && c === 0) continue; - 9
const above = r > 0 ? dp[r - 1][c] : 0; - 10
const left = c > 0 ? dp[r][c - 1] : 0; - 11
dp[r][c] = above + left; - 12
} - 13
} - 14
return dp[n - 1][m - 1]; - 15
}
Where people go wrong
Reading every 0 in the table as an obstacle. In the 5 by 5 table, row 4 column 1 holds 0 and is perfectly walkable; both of its ways in are blocked, so no route reaches it, and it then hands that 0 on to its own neighbours like any ordinary value. Whole regions of clear cells can go dark like this behind a couple of well-placed obstacles. The other half of the trap is muscle memory on the edges: filling the first row and first column with 1s is right only until an obstacle appears in one of them, and seeding 1s past that obstacle invents routes that walk straight through a wall.
Ask for the cheapest route rather than the number of routes, obstacles still forbidden.
The sum becomes this cell's cost plus a min, and a blocked cell becomes infinity rather than 0, because for a minimization 0 reads as a wonderfully cheap route while infinity is what 'no route at all' means. That is min-cost-path with the obstacle rule bolted on.
Remove every obstacle.
The blocked branch never fires, row 0 and column 0 fill with 1s on their own, and the table is count-all-paths-in-a-grid again, closed form included. Drag the slider down to 2 to watch it happen: the top-left 2 by 2 corner of this grid holds no obstacle, so its answer is the unobstructed 2.
Allow yourself to bulldoze at most one obstacle of your choice.
The state gains a dimension, dp[r][c][k] with k recording whether the one removal has been spent, so the same sweep runs twice over with the k = 1 layer able to step onto a blocked cell from the k = 0 layer.