Topics › Dynamic Programming › 2D/3D DP and DP on Grids
Unique paths II
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
A robot starts at the top-left cell of an m × n grid and wants to reach the bottom-right cell, moving only right or down. Some cells are blocked: grid[i][j] == 1 marks an obstacle and 0 a free cell. A path may not pass through a blocked cell. Count the distinct paths.
This follows LeetCode 63. Some versions (for example Coding Ninjas “Maze Obstacles”) mark blocked cells with -1 instead; only the obstacle test changes.
Constraints: 1 ≤ m, n ≤ 100, and the answer fits in an int. If the start or the target is blocked, the answer is 0.
Example 1
grid = [[0, 0, 0],
[0, 1, 0],
[0, 0, 0]]
answer = 2 // around the centre obstacle: right-right-down-down or down-down-right-right
Example 2
grid = [[0, 1],
[0, 0]]
answer = 1 // down, then right
Why the formula from Unique Paths fails
Without obstacles every arrangement of m - 1 downs and n - 1 rights is a valid path, so the answer is a binomial coefficient. With obstacles, some of those arrangements pass through a blocked cell, and how many do depends on where the obstacles are. There is no clean formula, but the DP needs only one extra line: a blocked cell has zero paths through it.
Approach 1: Recursion
Intuition. Let f(i, j) be the number of obstacle-free paths from (0, 0) to (i, j). As in the obstacle-free problem, the last move into (i, j) came from above or from the left, and those two groups of paths do not overlap. The only change: if (i, j) is blocked, no path ends there, so f(i, j) = 0 regardless of its neighbours.
f(i, j) = 0 if i < 0, j < 0, or grid[i][j] == 1
f(0, 0) = 1 (when the start is free)
f(i, j) = f(i - 1, j) + f(i, j - 1) otherwise
The order of the checks matters. The bounds test must come before grid[i][j] is read, and the obstacle test must come before the start test, so that a blocked start correctly returns 0.
Algorithm.
- If
i < 0,j < 0, or the cell is blocked, return 0. - If
(i, j) == (0, 0), return 1. - Return
f(i - 1, j) + f(i, j - 1).
static int recursive(int[][] grid) {
return paths(grid.length - 1, grid[0].length - 1, grid);
}
static int paths(int i, int j, int[][] grid) {
if (i < 0 || j < 0 || grid[i][j] == 1) return 0; // off the grid or blocked: no path goes through here
if (i == 0 && j == 0) return 1;
return paths(i - 1, j, grid) + paths(i, j - 1, grid);
}
Complexity. Time O(2^(m+n)) in the worst case (no obstacles). Space O(m + n) for the recursion stack.
Approach 2: Memoization
Intuition. The state is a cell, so there are m · n states, and the plain recursion revisits them many times. Cache them in dp[i][j]. Obstacles and out-of-range cells are answered by the base case before the cache is touched, so they never need a table entry.
Algorithm.
- Create
dp[m][n]filled with-1. - Apply the base cases, then return
dp[i][j]if filled; otherwise compute, store and return.
static int memoization(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[][] dp = new int[m][n];
for (int[] row : dp) Arrays.fill(row, -1);
return memo(m - 1, n - 1, grid, dp);
}
static int memo(int i, int j, int[][] grid, int[][] dp) {
if (i < 0 || j < 0 || grid[i][j] == 1) return 0;
if (i == 0 && j == 0) return 1;
if (dp[i][j] != -1) return dp[i][j];
return dp[i][j] = memo(i - 1, j, grid, dp) + memo(i, j - 1, grid, dp);
}
Complexity. Time O(m · n), space O(m · n) plus O(m + n) stack.
Approach 3: Tabulation
Intuition. Fill the table row by row, left to right, so the cell above and the cell to the left are always ready. A blocked cell gets 0, which automatically cuts off every path that would go through it: any cell after it can only add 0 from that direction.
Algorithm.
- For each cell in row-major order:
- if blocked,
dp[i][j] = 0; - else if it is the start,
dp[i][j] = 1; - else
dp[i][j] = (i > 0 ? dp[i - 1][j] : 0) + (j > 0 ? dp[i][j - 1] : 0).
- if blocked,
- Return
dp[m - 1][n - 1].
static int tabulation(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) { dp[i][j] = 0; continue; } // blocked cells hold 0 paths
if (i == 0 && j == 0) { dp[i][j] = 1; continue; }
int up = i > 0 ? dp[i - 1][j] : 0;
int left = j > 0 ? dp[i][j - 1] : 0;
dp[i][j] = up + left;
}
}
return dp[m - 1][n - 1];
}
Dry run on Example 1:
| j=0 | j=1 | j=2 | |
|---|---|---|---|
| i=0 | 1 | 1 | 1 |
| i=1 | 1 | 0 (blocked) | 1 |
| i=2 | 1 | 1 | 2 |
dp[1][2] = dp[0][2] + dp[1][1] = 1 + 0, and dp[2][2] = dp[1][2] + dp[2][1] = 1 + 1 = 2.
Complexity. Time O(m · n), space O(m · n).
Approach 4: Space optimized
Intuition. Exactly as in the obstacle-free version, a single array row is enough: before row[j] is updated it holds the value from the row above, and row[j - 1] already holds this row’s value to the left. A blocked cell simply resets row[j] to 0.
Start with row[0] = 1, meaning one way to “arrive” at the start. If the start is blocked, the very first cell visited sets it back to 0, and everything after stays 0.
Algorithm.
row = [1, 0, ..., 0].- For each grid row, for
jfrom 0 ton - 1: if the cell is blocked,row[j] = 0; otherwise, ifj > 0,row[j] += row[j - 1]. - Return
row[n - 1].
Column 0 has no left neighbour, so a free cell there keeps the value from above. That is right: the only way into column 0 is from above.
static int spaceOptimized(int[][] grid) {
int n = grid[0].length;
int[] row = new int[n];
row[0] = 1; // one way to "arrive" at the start, cleared below if it is blocked
for (int[] cells : grid) {
for (int j = 0; j < n; j++) {
if (cells[j] == 1) row[j] = 0;
else if (j > 0) row[j] += row[j - 1]; // from above (old row[j]) + from the left
}
}
return row[n - 1];
}
Complexity. Time O(m · n), space O(n).
Edge cases and pitfalls
- Blocked start or target. The answer is 0. In the recursion this depends on testing the obstacle before the start; in the tabulation, on testing the obstacle first in the loop body.
- Pre-filling the first row and column with 1. A common shortcut for the obstacle-free problem is to set row 0 and column 0 to 1. With obstacles that is wrong: every cell to the right of an obstacle in row 0 is unreachable. The code avoids special initialisation and lets the general rule handle the borders. The test
[[0, 0, 1, 0, 0]](a single row cut by a wall) checks this. - Boxed-in start. In
[[0, 1, 0], [1, 0, 0], [0, 0, 0]]both moves out of the start are blocked, so the answer is 0 even though the target is free. - Bounds before lookup. In the recursion,
grid[i][j]must not be read wheniorjis negative; the||chain stops at the first true condition. 1 × 1grid. One path if the cell is free, none if it is blocked.- Testing. Beyond the fixed cases, 300 seeded random grids of up to
6 × 6run through all four methods.
Related problems
- Grid Unique Paths : DP on Grids (DP8) is the obstacle-free version, with a combinatorial formula as well.
- Grid unique paths is that same problem as it appears in the SDE sheet.
- Minimum sum path in the matrix keeps the right/down moves but minimises a cost.
- Rat in a Maze lists the paths through a blocked maze with backtracking instead of counting them.
- Minimum Falling Path Sum is a grid DP with three moves per step and a variable start.