980. Unique Paths III
Problem Description
You are given an m x n
grid where each cell contains one of four possible values:
1
: The starting square (exactly one exists in the grid)2
: The ending square (exactly one exists in the grid)0
: Empty squares that can be walked through-1
: Obstacles that cannot be walked through
Your task is to find the number of unique paths from the starting square to the ending square that satisfy these conditions:
- You can only move in 4 directions (up, down, left, right)
- Every non-obstacle square must be visited exactly once
- This includes the starting square, ending square, and all empty squares (
0
)
In other words, you need to count all possible paths that:
- Start at the cell marked with
1
- End at the cell marked with
2
- Visit every cell that isn't an obstacle (
-1
) exactly one time - Only use horizontal and vertical movements (no diagonal moves)
For example, if you have a 2x3 grid with one obstacle, there might be multiple valid paths that visit all non-obstacle cells exactly once while moving from start to end. The function should return the total count of such valid paths.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves a grid where we need to traverse from one cell to another. While it's presented as a 2D grid, it can be viewed as a graph where each cell is a node and adjacent cells (up, down, left, right) are connected by edges.
However, let's also consider the alternative path through the flowchart:
Is it a graph?
- No: Even though there's grid traversal, let's treat this as a grid/array problem rather than a traditional graph problem.
Need to solve for kth smallest/largest?
- No: We're not looking for kth smallest/largest element; we're counting valid paths.
Involves Linked Lists?
- No: The problem uses a 2D grid, not linked lists.
Does the problem have small constraints?
- Yes: The problem involves finding ALL possible paths that visit every non-obstacle cell exactly once. This is essentially asking us to find all valid permutations of visiting cells, which typically has factorial complexity. The constraints must be small (usually m×n ≤ 20) for this to be feasible.
Brute force / Backtracking?
- Yes: We need to explore all possible paths from start to end, visiting each cell exactly once. This requires trying different paths, marking cells as visited, exploring further, and then unmarking them to try other possibilities - the classic backtracking pattern.
Conclusion: The flowchart correctly leads us to use a Backtracking approach. We need to:
- Start from the starting cell (marked as
1
) - Try all four directions recursively
- Keep track of visited cells
- Backtrack by unmarking visited cells when returning
- Count valid paths that reach the ending cell after visiting all non-obstacle cells
Intuition
The key insight is recognizing this as a path enumeration problem with strict visitation constraints. We need to find ALL valid paths, not just one, which immediately suggests we can't use a greedy approach or simple BFS/DFS that finds just one solution.
Think about what makes a path valid:
- It starts at cell marked
1
- It ends at cell marked
2
- It visits EVERY non-obstacle cell exactly once
The third constraint is crucial - we must visit every walkable cell. This means if we have n
walkable cells total (including start and end), our path must be exactly n
steps long. Any shorter path would miss some cells, and any longer path would revisit cells.
Since we need to explore all possible paths and keep track of which cells we've visited, backtracking becomes the natural choice. Why? Because at each step, we have up to 4 choices (up, down, left, right), and we need to:
- Try each valid direction
- Mark the cell as visited (so we don't revisit it in the current path)
- Recursively explore from the new position
- Unmark the cell when we backtrack (so other paths can use it)
The "unmarking" step is critical - it allows us to reuse cells in different paths. For example, if path A goes through cell (1,1)
early and fails, path B should still be able to use cell (1,1)
later in its traversal.
We can optimize by first counting all walkable cells. Then, when we reach the ending cell, we simply check if we've visited exactly the right number of cells. If k
represents the number of cells visited in our current path and cnt
is the number of empty cells (0
s), then a valid complete path has k = cnt + 2
(the +2
accounts for the start and end cells).
This approach systematically explores all possible paths through exhaustive search with backtracking, ensuring we find every valid solution.
Learn more about Backtracking patterns.
Solution Approach
The implementation uses depth-first search with backtracking to explore all possible paths. Let's walk through the key components:
Initial Setup: First, we traverse the entire grid to:
- Find the starting position
(x, y)
wheregrid[i][j] == 1
- Count the number of empty cells (
0
s) and store it incnt
- This preprocessing helps us know when we've visited all required cells
The DFS Function:
We define dfs(i, j, k)
where:
(i, j)
is the current cell positionk
is the number of cells visited so far in the current path
Base Case:
When we reach the ending cell (grid[i][j] == 2
), we check if k == cnt + 1
. Why cnt + 1
?
cnt
counts only the empty cells (0
s)- We also need to count the starting cell (
1
) - So a complete path visits
cnt + 1
cells before reaching the end - If this condition is met, we've found a valid path and return
1
, otherwise0
Recursive Exploration:
For non-ending cells, we explore all four directions using the dirs
array (-1, 0, 1, 0, -1)
. The pairwise
function creates pairs like (-1, 0)
, (0, 1)
, (1, 0)
, (0, -1)
representing up, right, down, left movements.
For each adjacent cell (x, y)
:
- Check if it's within bounds:
0 <= x < m and 0 <= y < n
- Check if it hasn't been visited:
(x, y) not in vis
- Check if it's not an obstacle:
grid[x][y] != -1
Backtracking Mechanism: When a valid adjacent cell is found:
- Mark it as visited:
vis.add((x, y))
- Recursively explore from that cell:
dfs(x, y, k + 1)
- After exploring, unmark it:
vis.remove((x, y))
The unmarking step is crucial for backtracking - it allows the cell to be used in other potential paths.
Tracking Visited Cells:
We use a set vis
to track visited cells in the current path. Sets provide O(1) lookup and modification, making them efficient for this purpose. We initialize it with the starting position since we begin our path there.
Final Computation:
We start the DFS from the starting position with k = 0
(since we're counting cells visited after the start). The function returns the total count of all valid paths from start to end that visit every non-obstacle cell exactly once.
The time complexity is O(3^(m×n)) in the worst case since at each cell we have at most 3 unvisited directions to explore (we can't go back), and space complexity is O(m×n) for the recursion stack and visited set.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider this 2×3 grid:
[1, 0, 0] [0, 0, 2]
Initial Setup:
- Starting position:
(0, 0)
where value is1
- Ending position:
(1, 2)
where value is2
- Count of empty cells (
0
s):cnt = 4
- Initialize visited set:
vis = {(0, 0)}
Goal: Find paths that visit all 6 cells exactly once (4 empty cells + start + end).
DFS Exploration from (0,0), k=0:
We check all 4 directions from (0, 0)
:
- Up
(-1, 0)
: Out of bounds ❌ - Right
(0, 1)
: Valid, value is0
✓ - Down
(1, 0)
: Valid, value is0
✓ - Left
(0, -1)
: Out of bounds ❌
Path 1: Going right first from (0,0) → (0,1)
- Mark
(0, 1)
as visited:vis = {(0, 0), (0, 1)}
- Call
dfs(0, 1, 1)
From (0, 1)
, we can go to (0, 2)
or (1, 1)
:
Path 1a: (0,0) → (0,1) → (0,2)
- Mark
(0, 2)
visited, calldfs(0, 2, 2)
- From
(0, 2)
, only(1, 2)
is valid (the end cell) - But going to
(1, 2)
withk=3
means we've only visited 4 cells total - We need to visit all 6 cells, so this path fails ❌
- Backtrack: unmark
(0, 2)
Path 1b: (0,0) → (0,1) → (1,1)
- Mark
(1, 1)
visited, calldfs(1, 1, 2)
- Continue exploring...
- Eventually find:
(0,0) → (0,1) → (1,1) → (1,0) → (0,2) → (1,2)
- When reaching
(1, 2)
withk=5
, we've visited 6 cells total ✓ - This is a valid path! Count = 1
After backtracking from Path 1, we explore:
Path 2: Going down first from (0,0) → (1,0)
- Mark
(1, 0)
as visited:vis = {(0, 0), (1, 0)}
- Call
dfs(1, 0, 1)
- Continue exploration...
- Eventually find:
(0,0) → (1,0) → (1,1) → (0,1) → (0,2) → (1,2)
- This is another valid path! Count = 2
Key Backtracking Example:
Notice how cell (0, 1)
is used in both valid paths:
- In Path 1: visited as the 2nd cell
- In Path 2: visited as the 4th cell
This is possible because after Path 1 completes, we backtrack and unmark all cells except the starting position, allowing Path 2 to use them in a different order.
Final Result: The algorithm returns 2, indicating there are exactly 2 unique paths from start to end that visit every cell exactly once.
Solution Implementation
1class Solution:
2 def uniquePathsIII(self, grid: List[List[int]]) -> int:
3 def dfs(row: int, col: int, steps: int) -> int:
4 """
5 Depth-first search to count valid paths.
6
7 Args:
8 row: Current row position
9 col: Current column position
10 steps: Number of cells visited so far
11
12 Returns:
13 Number of valid paths from current position
14 """
15 # Check if we reached the destination
16 if grid[row][col] == 2:
17 # Valid path only if we visited all empty cells plus start
18 return 1 if steps == empty_cells + 1 else 0
19
20 path_count = 0
21
22 # Try all four directions: up, right, down, left
23 for i in range(4):
24 next_row = row + directions[i]
25 next_col = col + directions[i + 1]
26
27 # Check if next cell is valid and unvisited
28 if (0 <= next_row < rows and
29 0 <= next_col < cols and
30 (next_row, next_col) not in visited and
31 grid[next_row][next_col] != -1):
32
33 # Mark cell as visited
34 visited.add((next_row, next_col))
35
36 # Recursively explore from next cell
37 path_count += dfs(next_row, next_col, steps + 1)
38
39 # Backtrack: unmark cell as visited
40 visited.remove((next_row, next_col))
41
42 return path_count
43
44 # Initialize grid dimensions
45 rows, cols = len(grid), len(grid[0])
46
47 # Find starting position (cell with value 1)
48 start_row, start_col = 0, 0
49 for i in range(rows):
50 for j in range(cols):
51 if grid[i][j] == 1:
52 start_row, start_col = i, j
53 break
54
55 # Direction vectors for moving up, right, down, left
56 directions = [-1, 0, 1, 0, -1]
57
58 # Count empty cells (cells with value 0)
59 empty_cells = sum(row.count(0) for row in grid)
60
61 # Initialize visited set with starting position
62 visited = {(start_row, start_col)}
63
64 # Start DFS from starting position with 0 steps taken
65 return dfs(start_row, start_col, 0)
66
1class Solution {
2 // Grid dimensions
3 private int rows;
4 private int cols;
5 // Count of empty cells that need to be visited
6 private int emptyCellCount;
7 // Reference to the input grid
8 private int[][] grid;
9 // Visited cells tracker for backtracking
10 private boolean[][] visited;
11
12 /**
13 * Find the number of unique paths from start to end visiting all non-obstacle cells.
14 * Grid values: 1 = start, 2 = end, 0 = empty cell, -1 = obstacle
15 *
16 * @param grid the input grid
17 * @return number of unique paths visiting all walkable cells exactly once
18 */
19 public int uniquePathsIII(int[][] grid) {
20 // Initialize grid dimensions
21 rows = grid.length;
22 cols = grid[0].length;
23 this.grid = grid;
24
25 // Variables to store starting position
26 int startRow = 0;
27 int startCol = 0;
28
29 // Find starting position and count empty cells
30 for (int i = 0; i < rows; i++) {
31 for (int j = 0; j < cols; j++) {
32 if (grid[i][j] == 0) {
33 // Count empty cells that must be visited
34 emptyCellCount++;
35 } else if (grid[i][j] == 1) {
36 // Record starting position
37 startRow = i;
38 startCol = j;
39 }
40 }
41 }
42
43 // Initialize visited array for backtracking
44 visited = new boolean[rows][cols];
45 // Mark starting position as visited
46 visited[startRow][startCol] = true;
47
48 // Start DFS from the starting position
49 return dfs(startRow, startCol, 0);
50 }
51
52 /**
53 * Depth-first search to explore all possible paths.
54 *
55 * @param row current row position
56 * @param col current column position
57 * @param visitedCount number of cells visited so far (excluding start)
58 * @return number of valid paths from current position
59 */
60 private int dfs(int row, int col, int visitedCount) {
61 // Check if we reached the destination
62 if (grid[row][col] == 2) {
63 // Valid path only if we visited all empty cells plus the start cell
64 return visitedCount == emptyCellCount + 1 ? 1 : 0;
65 }
66
67 int pathCount = 0;
68
69 // Direction arrays for moving up, right, down, left
70 int[] directionOffsets = {-1, 0, 1, 0, -1};
71
72 // Try all four directions
73 for (int dir = 0; dir < 4; dir++) {
74 int nextRow = row + directionOffsets[dir];
75 int nextCol = col + directionOffsets[dir + 1];
76
77 // Check if next position is valid:
78 // - Within grid boundaries
79 // - Not visited yet
80 // - Not an obstacle
81 if (nextRow >= 0 && nextRow < rows &&
82 nextCol >= 0 && nextCol < cols &&
83 !visited[nextRow][nextCol] &&
84 grid[nextRow][nextCol] != -1) {
85
86 // Mark as visited for this path
87 visited[nextRow][nextCol] = true;
88
89 // Recursively explore from the next position
90 pathCount += dfs(nextRow, nextCol, visitedCount + 1);
91
92 // Backtrack: unmark visited for other paths
93 visited[nextRow][nextCol] = false;
94 }
95 }
96
97 return pathCount;
98 }
99}
100
1class Solution {
2public:
3 int uniquePathsIII(vector<vector<int>>& grid) {
4 int rows = grid.size();
5 int cols = grid[0].size();
6
7 // Count the number of empty cells (cells with value 0)
8 int emptyCellCount = 0;
9 for (const auto& row : grid) {
10 for (int cell : row) {
11 if (cell == 0) {
12 emptyCellCount++;
13 }
14 }
15 }
16
17 // Direction vectors for moving up, right, down, left
18 int directions[5] = {-1, 0, 1, 0, -1};
19
20 // Visited array to track cells in current path
21 bool visited[rows][cols];
22 memset(visited, false, sizeof(visited));
23
24 // DFS function to explore all paths
25 // Parameters: current row, current column, number of cells visited so far
26 function<int(int, int, int)> dfs = [&](int row, int col, int cellsVisited) -> int {
27 // Base case: reached the ending square
28 if (grid[row][col] == 2) {
29 // Check if we've visited all non-obstacle cells
30 // cellsVisited should equal emptyCellCount + 1 (for the starting cell)
31 return cellsVisited == emptyCellCount + 1 ? 1 : 0;
32 }
33
34 int pathCount = 0;
35
36 // Try all four directions
37 for (int dir = 0; dir < 4; ++dir) {
38 int nextRow = row + directions[dir];
39 int nextCol = col + directions[dir + 1];
40
41 // Check if next cell is valid and can be visited
42 if (nextRow >= 0 && nextRow < rows &&
43 nextCol >= 0 && nextCol < cols &&
44 !visited[nextRow][nextCol] &&
45 grid[nextRow][nextCol] != -1) {
46
47 // Mark cell as visited
48 visited[nextRow][nextCol] = true;
49
50 // Recursively explore from the next cell
51 pathCount += dfs(nextRow, nextCol, cellsVisited + 1);
52
53 // Backtrack: unmark cell as visited
54 visited[nextRow][nextCol] = false;
55 }
56 }
57
58 return pathCount;
59 };
60
61 // Find the starting position (cell with value 1) and begin DFS
62 for (int i = 0; i < rows; ++i) {
63 for (int j = 0; j < cols; ++j) {
64 if (grid[i][j] == 1) {
65 // Mark starting cell as visited
66 visited[i][j] = true;
67 // Start DFS from the starting position
68 return dfs(i, j, 0);
69 }
70 }
71 }
72
73 return 0;
74 }
75};
76
1/**
2 * Counts the number of unique paths from start to end visiting all non-obstacle cells exactly once
3 * @param grid - 2D grid where 1 is start, 2 is end, 0 is empty cell, -1 is obstacle
4 * @returns Number of unique paths that visit all walkable cells exactly once
5 */
6function uniquePathsIII(grid: number[][]): number {
7 const rows: number = grid.length;
8 const cols: number = grid[0].length;
9
10 // Find starting position and count empty cells
11 let startRow: number = 0;
12 let startCol: number = 0;
13 let emptyCellCount: number = 0;
14
15 for (let row = 0; row < rows; row++) {
16 for (let col = 0; col < cols; col++) {
17 if (grid[row][col] === 0) {
18 emptyCellCount++;
19 } else if (grid[row][col] === 1) {
20 startRow = row;
21 startCol = col;
22 }
23 }
24 }
25
26 // Initialize visited matrix to track visited cells during DFS
27 const visited: boolean[][] = Array.from(
28 { length: rows },
29 () => Array(cols).fill(false)
30 );
31 visited[startRow][startCol] = true;
32
33 // Direction vectors for moving up, right, down, left
34 const directions: number[] = [-1, 0, 1, 0, -1];
35
36 /**
37 * Performs depth-first search to find all valid paths
38 * @param currentRow - Current row position
39 * @param currentCol - Current column position
40 * @param visitedCount - Number of cells visited so far
41 * @returns Number of valid paths from current position
42 */
43 const depthFirstSearch = (currentRow: number, currentCol: number, visitedCount: number): number => {
44 // Check if we reached the end
45 if (grid[currentRow][currentCol] === 2) {
46 // Valid path only if we visited all empty cells plus the start cell
47 return visitedCount === emptyCellCount + 1 ? 1 : 0;
48 }
49
50 let pathCount: number = 0;
51
52 // Try all four directions
53 for (let dir = 0; dir < 4; dir++) {
54 const nextRow: number = currentRow + directions[dir];
55 const nextCol: number = currentCol + directions[dir + 1];
56
57 // Check if next position is valid and unvisited
58 if (nextRow >= 0 && nextRow < rows &&
59 nextCol >= 0 && nextCol < cols &&
60 !visited[nextRow][nextCol] &&
61 grid[nextRow][nextCol] !== -1) {
62
63 // Mark as visited and explore
64 visited[nextRow][nextCol] = true;
65 pathCount += depthFirstSearch(nextRow, nextCol, visitedCount + 1);
66 // Backtrack: unmark as visited
67 visited[nextRow][nextCol] = false;
68 }
69 }
70
71 return pathCount;
72 };
73
74 // Start DFS from the starting position
75 return depthFirstSearch(startRow, startCol, 0);
76}
77
Time and Space Complexity
Time Complexity: O(3^(m × n))
The algorithm uses DFS with backtracking to explore all possible paths from the starting position to the ending position. In the worst case:
- Each cell (except obstacles) can be visited, giving us at most
m × n
cells to explore - From each cell, we can move in at most 3 directions (we cannot go back to where we came from due to the visited set)
- The recursion depth can be at most
m × n
(visiting all non-obstacle cells) - This creates a recursion tree where each node has at most 3 branches and the height is at most
m × n
- Therefore, the time complexity is
O(3^(m × n))
Space Complexity: O(m × n)
The space complexity consists of:
- The visited set
vis
which can contain at mostm × n
coordinates:O(m × n)
- The recursion call stack depth which can go up to
m × n
in the worst case when we need to visit all cells:O(m × n)
- Other variables like
start
,dirs
,cnt
which take constant space:O(1)
The dominant factor is O(m × n)
for both the visited set and the recursion stack, resulting in overall space complexity of O(m × n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Path Length Validation
The Problem: A frequent mistake is miscounting the total number of cells that need to be visited. Developers often forget to account for all walkable cells properly:
# INCORRECT: Only counting empty cells if grid[row][col] == 2: return 1 if steps == empty_cells else 0 # Wrong!
This fails because the path must include:
- The starting cell (value
1
) - All empty cells (value
0
) - The ending cell (value
2
)
The Solution:
The correct validation checks if we've visited empty_cells + 1
cells when reaching the end:
if grid[row][col] == 2: return 1 if steps == empty_cells + 1 else 0
The +1
accounts for the starting cell, and the ending cell is implicitly counted when we reach it.
Pitfall 2: Not Properly Backtracking the Visited Set
The Problem: Forgetting to remove cells from the visited set after exploring a path:
# INCORRECT: Missing backtrack visited.add((next_row, next_col)) path_count += dfs(next_row, next_col, steps + 1) # Forgot to remove from visited!
This causes cells to remain marked as visited even when exploring alternative paths, leading to undercounting valid paths.
The Solution: Always remove the cell from visited after the recursive call returns:
visited.add((next_row, next_col)) path_count += dfs(next_row, next_col, steps + 1) visited.remove((next_row, next_col)) # Critical backtracking step
Pitfall 3: Visiting the Starting Cell Multiple Times
The Problem: Not initializing the visited set with the starting position:
# INCORRECT: Empty visited set
visited = set() # Wrong!
return dfs(start_row, start_col, 0)
This allows the path to potentially revisit the starting cell, violating the "exactly once" constraint.
The Solution: Initialize the visited set with the starting position:
visited = {(start_row, start_col)} # Correct initialization return dfs(start_row, start_col, 0)
Pitfall 4: Using Wrong Direction Arrays
The Problem: Incorrectly implementing the direction vectors or their usage:
# INCORRECT: Wrong direction array usage directions = [(-1, 0), (0, 1), (1, 0), (0, -1)] # Pairs for dx, dy in directions: next_row = row + dx next_col = col + dy
While this works, mixing it with the flattened array approach can cause index errors:
# INCORRECT: Mismatched iteration
directions = [-1, 0, 1, 0, -1]
for i in range(4):
next_row = row + directions[i]
next_col = col + directions[i] # Wrong index!
The Solution: Use consistent indexing with the flattened array:
directions = [-1, 0, 1, 0, -1]
for i in range(4):
next_row = row + directions[i]
next_col = col + directions[i + 1] # Correct offset
Pitfall 5: Counting All Cells Instead of Empty Cells
The Problem: Counting all non-obstacle cells when determining the required path length:
# INCORRECT: Counting start and end cells
empty_cells = 0
for i in range(rows):
for j in range(cols):
if grid[i][j] != -1: # Wrong condition!
empty_cells += 1
This incorrectly includes the start (1) and end (2) cells in the count.
The Solution: Only count cells with value 0:
empty_cells = sum(row.count(0) for row in grid)
Or explicitly:
empty_cells = 0
for i in range(rows):
for j in range(cols):
if grid[i][j] == 0: # Only count empty cells
empty_cells += 1
A heap is a ...?
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
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!