Facebook Pixel

980. Unique Paths III

HardBit ManipulationArrayBacktrackingMatrix
Leetcode Link

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:

  1. Start at the cell marked with 1
  2. End at the cell marked with 2
  3. Visit every cell that isn't an obstacle (-1) exactly one time
  4. 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:

  1. Start from the starting cell (marked as 1)
  2. Try all four directions recursively
  3. Keep track of visited cells
  4. Backtrack by unmarking visited cells when returning
  5. Count valid paths that reach the ending cell after visiting all non-obstacle cells
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. It starts at cell marked 1
  2. It ends at cell marked 2
  3. 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 (0s), 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) where grid[i][j] == 1
  • Count the number of empty cells (0s) and store it in cnt
  • 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 position
  • k 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 (0s)
  • 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, otherwise 0

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):

  1. Check if it's within bounds: 0 <= x < m and 0 <= y < n
  2. Check if it hasn't been visited: (x, y) not in vis
  3. Check if it's not an obstacle: grid[x][y] != -1

Backtracking Mechanism: When a valid adjacent cell is found:

  1. Mark it as visited: vis.add((x, y))
  2. Recursively explore from that cell: dfs(x, y, k + 1)
  3. 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 Evaluator

Example 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 is 1
  • Ending position: (1, 2) where value is 2
  • Count of empty cells (0s): 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 is 0
  • Down (1, 0): Valid, value is 0
  • 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, call dfs(0, 2, 2)
  • From (0, 2), only (1, 2) is valid (the end cell)
  • But going to (1, 2) with k=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, call dfs(1, 1, 2)
  • Continue exploring...
  • Eventually find: (0,0) → (0,1) → (1,1) → (1,0) → (0,2) → (1,2)
  • When reaching (1, 2) with k=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 most m × 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

A heap is a ...?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More