Unique Paths II
You are given an m × n grid where each cell holds 0 for an empty square or 1 for an obstacle. A robot starts at the top-left cell and wants to reach the bottom-right cell, moving only right or down and never entering an obstacle. Return the number of distinct paths. If the start or the destination is itself an obstacle, the answer is 0.
- 1 ≤ m, n ≤ 100
- Each cell is 0 (empty) or 1 (obstacle)
- Movement is restricted to right and down only
- The answer fits in a signed 32-bit integer
grid = [[0,0,0],[0,1,0],[0,0,0]]2grid = [[0,1],[0,0]]1grid = [[1,0],[0,0]]0grid = [[0,0],[0,1]]0grid = [[0,0,0],[1,1,0],[0,0,0]]1A robot moves right or down through a grid, from top-left to bottom-right, counting the routes — but now some cells are blocked. The robot may not stand on a blocked cell, so no path may pass through one.
With obstacles there is no clever counting shortcut: whether a route is legal depends on exactly where the walls sit, so you cannot count routes without looking at the grid. The table, though, barely changes. That is a handy property of these tables — a new constraint usually just forbids some cells rather than reshaping the recurrence.
To be standing on a walkable cell (r, c), the robot's last move came from above or from the left. So the count is the sum of those two neighbours.
The only new rule: if (r, c) is an obstacle, no path can end there at all, so its count is 0.
f(r, c) is the number of obstacle-free paths from the start to cell (r, c), and it is 0 whenever (r, c) is blocked.f(r, c) = 0 if the cell is an obstacle, otherwise f(r-1, c) + f(r, c-1)
The base cases need care. Walking right along the top row, once you hit an obstacle every cell after it in that row is unreachable, because there is no row above to detour through. Same for the left column going down. If you blindly set the edges to all 1s, you will count paths that walk straight through a wall.
The cleanest way to avoid edge-case bugs is to let the recurrence handle the edges too, by treating anything off the grid as zero:
def unique_paths_with_obstacles(grid):
m, n = len(grid), len(grid[0])
if grid[0][0] == 1 or grid[m - 1][n - 1] == 1:
return 0 # blocked start or finish
dp = [[0] * n for _ in range(m)]
dp[0][0] = 1
for r in range(m):
for c in range(n):
if grid[r][c] == 1:
dp[r][c] = 0 # unreachable; contributes nothing
continue
if r > 0: dp[r][c] += dp[r - 1][c]
if c > 0: dp[r][c] += dp[r][c - 1]
return dp[m - 1][n - 1]The dp[0][0] = 1 before the loop is the empty path, and since (0,0) has no cell above or to its left, the loop leaves it alone. Every other edge cell now gets its value from its single in-grid neighbour, which is exactly right — including inheriting a zero if that neighbour was blocked.
The memoised top-down version says the same thing from the other end:
def unique_paths_with_obstacles(grid):
m, n = len(grid), len(grid[0])
memo = {}
def f(r, c):
if r < 0 or c < 0: return 0 # walked off the grid
if grid[r][c] == 1: return 0 # blocked
if r == 0 and c == 0: return 1 # reached the start
if (r, c) in memo: return memo[(r, c)]
memo[(r, c)] = f(r - 1, c) + f(r, c - 1)
return memo[(r, c)]
return f(m - 1, n - 1)Note the order of the checks: the obstacle test comes before the start test, so a blocked starting cell correctly returns 0 rather than 1.
The same overlapping-read trick works, with obstacles zeroing entries as we sweep:
def unique_paths_with_obstacles(grid):
n = len(grid[0])
row = [0] * n
row[0] = 1
for r in range(len(grid)):
for c in range(n):
if grid[r][c] == 1:
row[c] = 0 # this cell is unreachable
elif c > 0:
row[c] += row[c - 1]
return row[n - 1]When we reach column c, row[c] still holds the count from the row above and row[c-1] holds this row's already-updated value, so the single line adds exactly the two contributions we want. Setting row[c] = 0 on an obstacle wipes the inherited value from above, which is precisely the intended meaning: nothing can reach this cell, so nothing continues through it.
1 1 1 — the top row is clear, so one route to each.(1,0) = above 1 + nothing to the left = 1. (1,1) is an obstacle → 0. (1,2) = above 1 + left 0 = 1. Row: 1 0 1.(2,0) = above 1 = 1. (2,1) = above 0 + left 1 = 1. (2,2) = above 1 + left 1 = 2.Answer 2 — go around the obstacle above it, or below it. Watch how the zero at the centre propagated: (2,1) correctly received nothing from above, but still picked up the route coming along the bottom.
Now a fully blocking case, [[0,1],[1,0]]: (0,1) is blocked → 0, (1,0) is blocked → 0, so (1,1) = 0 + 0 = 0. The robot is walled in, and the table reports it without any special check.
And the degenerate case [[1]]: the start is itself an obstacle, so the answer is 0.
Two ideas.
Constraints usually enter these tables as forbidden states, not as new logic. When a problem adds "you may not do X", the first thing to try is giving the offending states a neutral value — 0 for counting problems, infinity for minimisation problems, negative infinity for maximisation — and letting the existing recurrence carry it. Nine times out of ten the arithmetic does the work for you, and you avoid a thicket of special cases.
Watch what a new constraint does to your base cases. The recurrence here barely changed, but the edges did, and that is where the bugs live. In the empty grid, the whole top row was 1 by symmetry. With obstacles, that assumption silently walks through walls. Whenever you adapt a solved problem, re-derive the base cases from scratch rather than copying them across.