63. Unique Paths II - Explanation
Description
You are given an m x n integer array grid. There is a robot initially located at the top-left corner (i.e., grid[0][0]). The robot tries to move to the bottom-right corner (i.e., grid[m - 1][n - 1]). The robot can only move either down or right at any point in time.
An obstacle and space are marked as 1 or 0 respectively in grid. A path that the robot takes cannot include any square that is an obstacle.
Return the number of possible unique paths that the robot can take to reach the bottom-right corner.
The testcases are generated so that the answer will be less than or equal to 2 * (10^9).
Example 1:
Input: obstacleGrid = [[0,0,0],[0,0,0],[0,1,0]]
Output: 3Explanation: There are three ways to reach the bottom-right corner:
- Right -> Right -> Down -> Down
- Right -> Down -> Right -> Down
- Down -> Right -> Right -> Down
Example 2:
Input: obstacleGrid = [[0,0,0],[0,0,1],[0,1,0]]
Output: 0Constraints:
m == obstacleGrid.lengthn == obstacleGrid[i].length1 <= m, n <= 100obstacleGrid[i][j]is0or1.
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Dynamic Programming - Understanding memoization (top-down) and tabulation (bottom-up) approaches
- 2D Grid Traversal - Navigating through rows and columns of a matrix
- Recursion - Building solutions by breaking problems into smaller subproblems
1. Dynamic Programming (Top-Down)
Intuition
We want to count all possible paths from the top-left corner to the bottom-right corner, but some cells are blocked by obstacles. At any cell, we can only move right or down. This naturally leads to a recursive approach: the number of paths from a cell equals the sum of paths from the cell below and the cell to the right. If we hit an obstacle or go out of bounds, that path contributes 0. Since many subproblems overlap (the same cell gets visited through different routes), we use memoization to avoid redundant calculations.
Algorithm
- Define a recursive function
dfs(r, c)that returns the number of paths from cell(r, c)to the destination. - Base cases:
- If
rorcis out of bounds, or the cell contains an obstacle, return0. - If we reach the destination
(M-1, N-1), return1.
- If
- If the result for
(r, c)is already indp, return the cached value. - Otherwise, compute
dfs(r+1, c) + dfs(r, c+1)and store it indp. - Call
dfs(0, 0)to get the total number of unique paths.
class Solution:
def uniquePathsWithObstacles(self, grid: List[List[int]]) -> int:
M, N = len(grid), len(grid[0])
dp = {(M - 1, N - 1): 1}
def dfs(r, c):
if r == M or c == N or grid[r][c]:
return 0
if (r, c) in dp:
return dp[(r, c)]
dp[(r, c)] = dfs(r + 1, c) + dfs(r, c + 1)
return dp[(r, c)]
return dfs(0, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows and is the number of columns.
2. Dynamic Programming (Bottom-Up)
Intuition
Instead of solving recursively from the start, we can build the solution iteratively from the destination back to the start. Each cell stores the number of ways to reach the destination from that cell. For any cell, this count is the sum of the counts from the cell below and the cell to the right. Obstacles simply have a count of 0 since no path can go through them.
Algorithm
- If the start or destination cell has an obstacle, return
0immediately. - Create a 2D
dptable with an extra row and column (initialized to0) for boundary handling. - Set
dp[M-1][N-1] = 1since there is exactly one way to reach the destination from itself. - Iterate from the bottom-right to the top-left:
- If the current cell has an obstacle, set
dp[r][c] = 0. - Otherwise, set
dp[r][c] = dp[r+1][c] + dp[r][c+1].
- If the current cell has an obstacle, set
- Return
dp[0][0]as the answer.
class Solution:
def uniquePathsWithObstacles(self, grid: List[List[int]]) -> int:
M, N = len(grid), len(grid[0])
if grid[0][0] == 1 or grid[M - 1][N - 1] == 1:
return 0
dp = [[0] * (N + 1) for _ in range(M + 1)]
dp[M - 1][N - 1] = 1
for r in range(M - 1, -1, -1):
for c in range(N - 1, -1, -1):
if grid[r][c] == 1:
dp[r][c] = 0
else:
dp[r][c] += dp[r + 1][c]
dp[r][c] += dp[r][c + 1]
return dp[0][0]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows and is the number of columns.
3. Dynamic Programming (Space Optimized)
Intuition
Looking at the bottom-up approach, we notice that each cell only depends on the cell directly below it and the cell to its right. Since we process row by row from bottom to top, we only need to keep track of one row at a time. The value dp[c] before updating represents the count from the row below, and dp[c+1] after updating represents the count from the right. This reduces space from O(m * n) to O(n).
Algorithm
- Create a 1D array
dpof sizeN+1, initialized to0. - Set
dp[N-1] = 1to represent the destination. - Iterate through each row from bottom to top:
- For each column
cfrom right to left:- If the cell has an obstacle, set
dp[c] = 0. - Otherwise, add
dp[c+1]todp[c](accumulating paths from below and right).
- If the cell has an obstacle, set
- For each column
- Return
dp[0]as the final answer.
class Solution:
def uniquePathsWithObstacles(self, grid: List[List[int]]) -> int:
M, N = len(grid), len(grid[0])
dp = [0] * (N + 1)
dp[N - 1] = 1
for r in range(M - 1, -1, -1):
for c in range(N - 1, -1, -1):
if grid[r][c]:
dp[c] = 0
else:
dp[c] += dp[c + 1]
return dp[0]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows and is the number of columns.
4. Dynamic Programming (In-Place)
Intuition
We can avoid using any extra space by reusing the input grid itself to store the path counts. The key insight is that once we process a cell, we no longer need its original value (which was just 0 or 1 for obstacle). We transform grid so each cell holds the number of paths from that cell to the destination. Obstacles get converted to 0 since no path passes through them.
Algorithm
- If the start or destination has an obstacle, return
0. - Set
grid[M-1][N-1] = 1to mark the destination. - Iterate from the bottom-right to the top-left (skipping the destination cell):
- If the cell is an obstacle, set it to
0. - Otherwise, compute
down + rightwheredownis the cell below andrightis the cell to the right.
- If the cell is an obstacle, set it to
- Return
grid[0][0]as the answer.
class Solution:
def uniquePathsWithObstacles(self, grid: List[List[int]]) -> int:
M, N = len(grid), len(grid[0])
if grid[0][0] == 1 or grid[M - 1][N - 1] == 1:
return 0
grid[M - 1][N - 1] = 1
for r in range(M - 1, -1, -1):
for c in range(N - 1, -1, -1):
if r == M - 1 and c == N - 1:
continue
if grid[r][c] == 1:
grid[r][c] = 0
else:
down = grid[r + 1][c] if r + 1 < M else 0
right = grid[r][c + 1] if c + 1 < N else 0
grid[r][c] = down + right
return grid[0][0]Time & Space Complexity
- Time complexity:
- Space complexity: extra space.
Where is the number of rows and is the number of columns.
Common Pitfalls
Not Checking Start or End for Obstacles
If either the starting cell grid[0][0] or the destination cell grid[M-1][N-1] contains an obstacle, there are zero paths. Forgetting this check leads to incorrect results.
Incorrect Base Case Initialization
When filling the first row or first column, all cells after an obstacle should have zero paths. A common mistake is initializing the entire edge with 1s without considering that obstacles block all subsequent cells.
# Wrong: doesn't account for obstacles blocking the path
for c in range(N):
dp[0][c] = 1
# Correct: stop when hitting an obstacle
for c in range(N):
if grid[0][c] == 1:
break
dp[0][c] = 1Confusing Obstacle Value with Path Count
In the in-place approach, obstacles are marked as 1 in the input but need to become 0 in the DP array. Confusing these values causes obstacles to be counted as having one path.
Off-by-One Errors in Grid Iteration
When iterating bottom-up or right-to-left, ensure loop bounds are correct. Starting at M-1 and going to 0 requires range(M-1, -1, -1) in Python, not range(M-1, 0, -1) which skips the first row.
Sign in to join the discussion