63. Unique Paths II
Problem Description
You have a robot placed on an m x n
grid, where the robot starts at the top-left corner position grid[0][0]
. The robot's goal is to reach the bottom-right corner position grid[m-1][n-1]
.
The robot has movement restrictions:
- It can only move in two directions: either down (to the next row) or right (to the next column)
- It cannot move up or left
- It cannot move diagonally
The grid contains obstacles and empty spaces:
- A cell with value
1
represents an obstacle that blocks the robot's path - A cell with value
0
represents an empty space where the robot can move through
The robot must find a path from start to finish without passing through any obstacles. Your task is to calculate the total number of unique paths the robot can take to reach the destination.
For example, if the robot is at position (i, j)
, it can only move to:
- Position
(i+1, j)
- moving down - Position
(i, j+1)
- moving right
But only if those positions are within the grid boundaries and don't contain obstacles.
The function should return the total count of all possible unique paths. If there's no valid path (due to obstacles blocking all routes), it should return 0
.
Intuition
When thinking about how to count all possible paths, we can observe that this is a classic dynamic programming problem. The key insight is that the number of ways to reach any cell is the sum of ways to reach it from the cells that can lead to it.
Since the robot can only move down or right, any cell (i, j)
can only be reached from:
- The cell above it
(i-1, j)
by moving down - The cell to its left
(i, j-1)
by moving right
This means if we know the number of paths to reach (i-1, j)
and (i, j-1)
, we can calculate the paths to (i, j)
by simply adding them together: paths[i][j] = paths[i-1][j] + paths[i][j-1]
.
However, we need to handle obstacles - if a cell contains an obstacle (value 1
), there are 0
ways to reach the destination through that cell.
We can solve this problem recursively by breaking it down:
- From any position
(i, j)
, the robot has two choices: go down to(i+1, j)
or go right to(i, j+1)
- The total paths from
(i, j)
equals the sum of paths from these two next positions - We work backwards from the destination, asking "how many ways can I reach the end from here?"
The base cases are:
- If we're out of bounds or hit an obstacle, return
0
(no valid path) - If we're at the destination
(m-1, n-1)
, return1
(we found one valid path)
To avoid recalculating the same subproblems multiple times (since many paths might pass through the same cell), we use memoization with the @cache
decorator. This stores previously computed results and reuses them when needed, making our solution efficient.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses a memoization search (also known as top-down dynamic programming) approach with depth-first search (DFS).
We implement a recursive function dfs(i, j)
that calculates the number of unique paths from position (i, j)
to the destination (m-1, n-1)
.
Function Logic:
The dfs(i, j)
function follows these steps:
- Boundary and Obstacle Check:
- If
i >= m
orj >= n
(out of bounds), return0
- If
obstacleGrid[i][j] == 1
(obstacle present), return0
- If
- Destination Check:
- If
i == m-1
andj == n-1
(reached destination), return1
- If
- Recursive Case:
- Calculate paths by moving down:
dfs(i+1, j)
- Calculate paths by moving right:
dfs(i, j+1)
- Return the sum:
dfs(i+1, j) + dfs(i, j+1)
- Calculate paths by moving down:
Memoization with @cache:
The @cache
decorator automatically stores the results of dfs(i, j)
for each unique (i, j)
pair. When the same position is encountered again, the cached result is returned immediately instead of recalculating. This optimization reduces the time complexity from exponential to O(m × n)
.
Implementation Flow:
- Extract grid dimensions:
m = len(obstacleGrid)
andn = len(obstacleGrid[0])
- Start the recursive search from the top-left corner:
dfs(0, 0)
- The function explores all possible paths while avoiding obstacles
- The memoization ensures each cell is computed only once
- Return the total number of unique paths found
This approach efficiently handles the obstacle constraints while counting all valid paths from start to finish.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider a 3×3 grid with one obstacle:
[ [0, 0, 0], [0, 1, 0], [0, 0, 0] ]
Where 0 = empty space, 1 = obstacle
Step-by-step execution of dfs(0, 0)
:
-
Start at (0,0): Not out of bounds, no obstacle, not at destination
- Need to calculate:
dfs(1, 0) + dfs(0, 1)
- Need to calculate:
-
Branch 1: Calculate
dfs(1, 0)
(moving down from start)- Not out of bounds, no obstacle, not at destination
- Need to calculate:
dfs(2, 0) + dfs(1, 1)
2a. Calculate
dfs(2, 0)
: - Need:dfs(3, 0) + dfs(2, 1)
-dfs(3, 0)
= 0 (out of bounds) -dfs(2, 1)
needs calculation (see below)2b. Calculate
dfs(1, 1)
: - Returns 0 (obstacle at position [1][1]) -
Branch 2: Calculate
dfs(0, 1)
(moving right from start)- Not out of bounds, no obstacle, not at destination
- Need to calculate:
dfs(1, 1) + dfs(0, 2)
3a.
dfs(1, 1)
= 0 (already cached - obstacle)3b. Calculate
dfs(0, 2)
: - Need:dfs(1, 2) + dfs(0, 3)
-dfs(0, 3)
= 0 (out of bounds) -dfs(1, 2)
needs calculation -
Continue exploring valid paths...
dfs(1, 2)
→ leads todfs(2, 2)
(destination) = 1dfs(2, 1)
→ leads todfs(2, 2)
(destination) = 1
Path visualization:
- Path 1: (0,0) → (0,1) → (0,2) → (1,2) → (2,2) ✓
- Path 2: (0,0) → (0,1) → (1,0) → (2,0) → (2,1) → (2,2) ✓
- Invalid: Any path through (1,1) is blocked by obstacle ✗
Memoization in action:
- When we first calculate
dfs(1, 1)
, it returns 0 due to the obstacle - Later calls to
dfs(1, 1)
immediately return the cached value 0 - Similarly,
dfs(2, 2)
is calculated once as 1 (destination reached) and cached
Final result: dfs(0, 0)
returns 2, indicating there are exactly 2 unique paths from start to finish while avoiding the obstacle.
Solution Implementation
1class Solution:
2 def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
3 """
4 Find the number of unique paths from top-left to bottom-right corner
5 in a grid with obstacles. Can only move right or down.
6
7 Args:
8 obstacleGrid: 2D grid where 1 represents obstacle, 0 represents empty cell
9
10 Returns:
11 Number of unique paths from (0, 0) to (m-1, n-1)
12 """
13 from functools import cache
14
15 # Get grid dimensions
16 rows = len(obstacleGrid)
17 cols = len(obstacleGrid[0])
18
19 @cache
20 def dfs(row: int, col: int) -> int:
21 """
22 Depth-first search with memoization to count paths from (row, col) to destination.
23
24 Args:
25 row: Current row position
26 col: Current column position
27
28 Returns:
29 Number of valid paths from current position to destination
30 """
31 # Base case: out of bounds or hit an obstacle
32 if row >= rows or col >= cols or obstacleGrid[row][col] == 1:
33 return 0
34
35 # Base case: reached destination
36 if row == rows - 1 and col == cols - 1:
37 return 1
38
39 # Recursive case: sum of paths going down and going right
40 paths_going_down = dfs(row + 1, col)
41 paths_going_right = dfs(row, col + 1)
42
43 return paths_going_down + paths_going_right
44
45 # Start DFS from top-left corner
46 return dfs(0, 0)
47
1class Solution {
2 // Memoization table to store computed results for subproblems
3 private Integer[][] memo;
4 // Reference to the input obstacle grid
5 private int[][] obstacleGrid;
6 // Number of rows in the grid
7 private int rows;
8 // Number of columns in the grid
9 private int cols;
10
11 /**
12 * Calculates the number of unique paths from top-left to bottom-right
13 * in a grid with obstacles.
14 *
15 * @param obstacleGrid 2D array where 1 represents an obstacle and 0 represents an empty cell
16 * @return Number of unique paths from (0,0) to (m-1,n-1)
17 */
18 public int uniquePathsWithObstacles(int[][] obstacleGrid) {
19 // Initialize grid dimensions
20 rows = obstacleGrid.length;
21 cols = obstacleGrid[0].length;
22
23 // Store reference to obstacle grid
24 this.obstacleGrid = obstacleGrid;
25
26 // Initialize memoization table
27 memo = new Integer[rows][cols];
28
29 // Start DFS from top-left corner (0, 0)
30 return dfs(0, 0);
31 }
32
33 /**
34 * Recursive helper function using DFS with memoization to count paths.
35 *
36 * @param row Current row position
37 * @param col Current column position
38 * @return Number of unique paths from current position to destination
39 */
40 private int dfs(int row, int col) {
41 // Base case: Out of bounds or obstacle encountered
42 if (row >= rows || col >= cols || obstacleGrid[row][col] == 1) {
43 return 0;
44 }
45
46 // Base case: Reached destination (bottom-right corner)
47 if (row == rows - 1 && col == cols - 1) {
48 return 1;
49 }
50
51 // Check if result already computed (memoization)
52 if (memo[row][col] == null) {
53 // Calculate paths by moving down or right
54 int pathsDown = dfs(row + 1, col);
55 int pathsRight = dfs(row, col + 1);
56
57 // Store the sum of paths in memoization table
58 memo[row][col] = pathsDown + pathsRight;
59 }
60
61 // Return memoized result
62 return memo[row][col];
63 }
64}
65
1class Solution {
2public:
3 int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
4 int rows = obstacleGrid.size();
5 int cols = obstacleGrid[0].size();
6
7 // Initialize memoization table with -1 (unvisited state)
8 vector<vector<int>> memo(rows, vector<int>(cols, -1));
9
10 // Define recursive DFS function with memoization
11 auto dfs = [&](this auto&& dfs, int row, int col) -> int {
12 // Base case: out of bounds or obstacle encountered
13 if (row >= rows || col >= cols || obstacleGrid[row][col] == 1) {
14 return 0;
15 }
16
17 // Base case: reached destination (bottom-right corner)
18 if (row == rows - 1 && col == cols - 1) {
19 return 1;
20 }
21
22 // Check if already computed
23 if (memo[row][col] == -1) {
24 // Compute and store the result: sum of paths going down and right
25 memo[row][col] = dfs(row + 1, col) + dfs(row, col + 1);
26 }
27
28 return memo[row][col];
29 };
30
31 // Start DFS from top-left corner (0, 0)
32 return dfs(0, 0);
33 }
34};
35
1/**
2 * Calculates the number of unique paths from top-left to bottom-right in a grid with obstacles
3 * @param obstacleGrid - 2D array where 0 represents empty cell and 1 represents obstacle
4 * @returns Number of unique paths from (0,0) to (m-1,n-1)
5 */
6function uniquePathsWithObstacles(obstacleGrid: number[][]): number {
7 const rows: number = obstacleGrid.length;
8 const cols: number = obstacleGrid[0].length;
9
10 // Memoization table to store computed results
11 // -1 indicates unvisited state
12 const memo: number[][] = Array.from(
13 { length: rows },
14 () => Array(cols).fill(-1)
15 );
16
17 /**
18 * Depth-first search with memoization to count paths
19 * @param row - Current row position
20 * @param col - Current column position
21 * @returns Number of valid paths from current position to destination
22 */
23 const dfs = (row: number, col: number): number => {
24 // Check boundaries and obstacles
25 if (row >= rows || col >= cols || obstacleGrid[row][col] === 1) {
26 return 0;
27 }
28
29 // Reached destination (bottom-right corner)
30 if (row === rows - 1 && col === cols - 1) {
31 return 1;
32 }
33
34 // Use memoized result if already computed
35 if (memo[row][col] === -1) {
36 // Calculate paths: move down + move right
37 memo[row][col] = dfs(row + 1, col) + dfs(row, col + 1);
38 }
39
40 return memo[row][col];
41 };
42
43 // Start DFS from top-left corner (0, 0)
44 return dfs(0, 0);
45}
46
Time and Space Complexity
Time Complexity: O(m × n)
The solution uses a depth-first search (DFS) with memoization via the @cache
decorator. Each cell (i, j)
in the grid is visited at most once due to memoization. When we reach a cell for the first time, we compute its result and cache it. Any subsequent calls to the same cell will return the cached result in O(1)
time. Since there are m × n
cells in total, and each cell requires O(1)
work (aside from the recursive calls), the overall time complexity is O(m × n)
.
Space Complexity: O(m × n)
The space complexity consists of two components:
- Cache storage: The
@cache
decorator stores the result for each unique(i, j)
pair. In the worst case, we may need to store results for allm × n
cells, requiringO(m × n)
space. - Recursion stack: In the worst case, the recursion depth can go up to
m + n - 1
(when traversing from top-left to bottom-right corner), which requiresO(m + n)
stack space.
Since O(m × n)
dominates O(m + n)
, the overall space complexity is O(m × n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Checking if Start or End Position is Blocked
One of the most common mistakes is forgetting to handle edge cases where the starting position (0, 0)
or ending position (m-1, n-1)
contains an obstacle. If either of these critical positions is blocked, there's no valid path possible, but the recursive solution might not handle this explicitly.
Problem Example:
obstacleGrid = [[1, 0], # Start position blocked! [0, 0]] # Or obstacleGrid = [[0, 0], [0, 1]] # End position blocked!
Solution: Add an early return check before starting the DFS:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
rows = len(obstacleGrid)
cols = len(obstacleGrid[0])
# Early return if start or end is blocked
if obstacleGrid[0][0] == 1 or obstacleGrid[rows-1][cols-1] == 1:
return 0
# Continue with DFS...
2. Stack Overflow with Large Grids
Python has a default recursion limit (typically 1000), and for large grids, the recursive DFS approach can hit this limit, causing a RecursionError
. A grid of size 100×100 could potentially require deep recursion.
Solution: Either increase the recursion limit or use an iterative DP approach:
import sys
sys.setrecursionlimit(10000) # Increase limit
# Or better: Use bottom-up DP instead
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
rows, cols = len(obstacleGrid), len(obstacleGrid[0])
if obstacleGrid[0][0] == 1 or obstacleGrid[rows-1][cols-1] == 1:
return 0
dp = [[0] * cols for _ in range(rows)]
dp[0][0] = 1
# Fill first row
for j in range(1, cols):
dp[0][j] = dp[0][j-1] if obstacleGrid[0][j] == 0 else 0
# Fill first column
for i in range(1, rows):
dp[i][0] = dp[i-1][0] if obstacleGrid[i][0] == 0 else 0
# Fill rest of the grid
for i in range(1, rows):
for j in range(1, cols):
if obstacleGrid[i][j] == 0:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[rows-1][cols-1]
3. Memory Issues with @cache Decorator
The @cache
decorator stores all computed results indefinitely, which can cause memory issues for very large grids or when the function is called multiple times with different grids in a testing environment.
Solution:
Clear the cache after each function call or use @lru_cache
with a size limit:
from functools import lru_cache
@lru_cache(maxsize=128) # Limited cache size
def dfs(row: int, col: int) -> int:
# ... function body ...
# Or clear cache after use:
result = dfs(0, 0)
dfs.cache_clear() # Clear the cache
return result
4. Integer Overflow for Large Path Counts
While Python handles arbitrary precision integers automatically, in other languages or when interfacing with systems expecting fixed-size integers, the path count might overflow for large grids without obstacles.
Solution: If needed, return the result modulo a large prime number:
MOD = 10**9 + 7 return (paths_going_down + paths_going_right) % MOD
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!