Unique Paths III
HardPrompt
You are given an m x n integer array grid where grid[i][j] could be:
1representing the starting square. There is exactly one starting square.2representing the ending square. There is exactly one ending square.0representing empty squares we can walk over.-1representing obstacles that we cannot walk over.
Return the number of 4-directional walks from the starting square to the ending square, that walk over every non-obstacle square exactly once.
Example 1:
Input: grid = [[1,0,0,0],[0,0,0,0],[0,0,2,-1]]
Output: 2
Explanation: We have the following two paths:
1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2)
2. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2)Example 2:
Input: grid = [[1,0,0,0],[0,0,0,0],[0,0,0,2]]
Output: 4
Explanation: We have the following four paths:
1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2),(2,3)
2. (0,0),(0,1),(1,1),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,3),(1,3),(2,3)
3. (0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(1,1),(0,1),(0,2),(0,3),(1,3),(2,3)
4. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2),(2,3)Example 3:
Input: grid = [[0,1],[2,0]]
Output: 0
Explanation: There is no path that walks over every empty square exactly once.
Note that the starting and ending square can be anywhere in the grid.
Constraints:
m == grid.lengthn == grid[i].length1 <= m, n <= 201 <= m * n <= 20-1 <= grid[i][j] <= 2- There is exactly one starting cell and one ending cell.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a standard backtracking algorithm, which is a form of Depth-First Search (DFS), to explore all possible paths from the starting square. The core idea is to try moving in all four directions from the current square. If a move leads to a valid, unvisited square, we recursively explore from there. To ensure we visit every non-obstacle square exactly once, we keep track of the number of squares visited. When we reach the ending square, we check if the number of visited squares matches the total number of non-obstacle squares. To avoid using a separate visited array and to handle backtracking efficiently, we can modify the grid in-place, marking a square as visited and then restoring its original value after exploring all paths from it.
Algorithm
- Initialization:
- Iterate through the grid to find the starting coordinates
(startX, startY). - Count the total number of walkable squares. This includes the starting square and all empty squares (
0). Let's call thiswalkableSquares.
- Iterate through the grid to find the starting coordinates
- Recursive Function (DFS):
- Define a recursive function,
dfs(grid, x, y, count), where(x, y)is the current cell andcountis the number of unique cells visited so far in the current path.
- Define a recursive function,
- Base Cases:
- Out of Bounds / Obstacle: If the current cell
(x, y)is outside the grid boundaries or is an obstacle (-1), the path is invalid. Stop this path by returning. - End Square: If the current cell is the end square (
2):- Check if
countis equal towalkableSquares. If they match, it means we have traversed all required squares exactly once before reaching the end. This is a valid path, so we increment a global counter. - Return, as the path ends here.
- Check if
- Out of Bounds / Obstacle: If the current cell
- Recursive Step:
- Mark as Visited: To avoid cycles and re-visiting squares in the same path, mark the current square
(x, y)as visited. A common technique is to temporarily change its value to-1(since obstacles are already handled). - Explore Neighbors: Recursively call the
dfsfunction for all four adjacent neighbors (up, down, left, right), incrementing thecountby 1 for each call:dfs(grid, newX, newY, count + 1). - Backtrack: After the recursive calls for all neighbors have returned, restore the original value of the cell
(x, y). This is the crucial backtracking step, allowing the cell to be used in other potential paths.
- Mark as Visited: To avoid cycles and re-visiting squares in the same path, mark the current square
Walkthrough
Algorithm
-
Pre-computation: First, we scan the entire grid to find the starting coordinates
(startX, startY)and to count the total number of squares we need to walk over. This count, let's call itwalkableSquares, is the number of0s plus the starting square1. A valid path must visit exactly this many squares before hitting the end square. -
DFS Function: We define a recursive helper function,
dfs(x, y, walkCount), which will perform the search.(x, y)are the current coordinates, andwalkCountis the number of squares visited so far in the current path. -
DFS Logic:
- Base Case 1 (Invalid Move): If
(x, y)is out of the grid boundaries or ifgrid[x][y]is an obstacle (-1), the path is invalid, so we return. - Base Case 2 (End Square): If
grid[x][y]is the end square (2), we check if we have visited all required squares. A valid path is found ifwalkCountequalswalkableSquares. If so, we increment our global result counter. In either case, we return because a path cannot continue past the end square. - Recursive Step:
a. Mark as Visited: Mark the current square
(x, y)as visited to prevent cycles. A simple way is to change its value to-1. b. Explore Neighbors: Explore the four adjacent squares (up, down, left, right) by making a recursive calldfs(newX, newY, walkCount + 1)for each neighbor. c. Backtrack: After the recursive calls for all neighbors return, we must "un-visit" the current square by restoring its original value. This is crucial for allowing other paths to use this square.
- Base Case 1 (Invalid Move): If
Code Snippet
class Solution { private int result = 0; private int walkableSquares = 1; // Start with 1 for the starting square itself public int uniquePathsIII(int[][] grid) { int startX = 0, startY = 0; int m = grid.length; int n = grid[0].length; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == 0) { walkableSquares++; } else if (grid[i][j] == 1) { startX = i; startY = j; } } } dfs(grid, startX, startY, 1); return result; } private void dfs(int[][] grid, int x, int y, int count) { // Base case: out of bounds or obstacle or already visited if (x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] == -1) { return; } // Base case: reached the end if (grid[x][y] == 2) { if (count == walkableSquares) { result++; } return; } // Mark the current cell as visited int originalValue = grid[x][y]; grid[x][y] = -1; // Explore neighbors dfs(grid, x + 1, y, count + 1); dfs(grid, x - 1, y, count + 1); dfs(grid, x, y + 1, count + 1); dfs(grid, x, y - 1, count + 1); // Backtrack: un-mark the cell grid[x][y] = originalValue; }}Complexity
Time
O(3^(m*n)) - In the worst case, from each non-obstacle square, we can explore up to 3 new directions (we don't go back to the square we just came from). If there are `k` walkable squares, the complexity is roughly `O(3^k)`. Since `k` can be up to `m*n`, the complexity is `O(3^(m*n))`. This is an upper bound; the actual performance is faster due to pruning.
Space
O(m * n) - The space complexity is determined by the maximum depth of the recursion stack. In the worst-case scenario, the path could cover all the squares in the grid, leading to a recursion depth of `m * n`.
Trade-offs
Pros
Relatively simple to understand and implement.
Low memory usage as it only uses the recursion stack for space.
Cons
The time complexity is exponential, making it inefficient for larger grids (though it passes with the given constraints).
It may re-explore the same sub-paths multiple times, as it doesn't store the results of subproblems.
This approach is a more optimized solution that uses dynamic programming combined with bitmasking to avoid re-computing results for the same subproblems. A subproblem is defined by the current position and the set of visited squares. Since the total number of squares (m*n) is small (<= 20), we can represent the set of visited squares using a bitmask, where the i-th bit is 1 if the i-th square has been visited, and 0 otherwise. This method effectively solves the Hamiltonian Path problem on the grid graph, which is feasible due to the small constraints.
Algorithm
- State Definition:
- We define a DP table,
dp[mask][i], which stores the number of paths that visit the set of squares represented bymaskand end at squarei. - We flatten the 2D grid coordinates
(r, c)into a single indexi = r * n + c. - The
maskis an integer where thej-th bit is 1 if squarejhas been visited, and 0 otherwise.
- We define a DP table,
- Initialization:
- First, iterate through the grid to find the start
(sr, sc)and end(er, ec)coordinates. Convert them tostartIndexandendIndex. - Also, compute a
targetMask. ThetargetMaskwill have a bit set to 1 for every non-obstacle square (0,1, or2). - Initialize the DP table:
dp[1 << startIndex][startIndex] = 1. This signifies that there is one path (of length 1) that has visited only the start square and ends at the start square.
- First, iterate through the grid to find the start
- Transitions:
- Iterate through all possible masks from
1to(1 << (m*n)) - 1. - For each
mask, iterate through each squareufrom0tom*n - 1. - If
dp[mask][u] > 0, it means we have valid paths ending at squareuwith visited setmask. - From square
u, try to move to its 4-directional neighborsv. - If neighbor
vis a valid, non-obstacle square and has not been visited yet in the currentmask(i.e.,(mask & (1 << v)) == 0), we can extend the path. - Create a
newMask = mask | (1 << v). - Update the DP table for the new state:
dp[newMask][v] += dp[mask][u].
- Iterate through all possible masks from
- Final Result:
- After filling the DP table, the final answer is the number of paths that visit all non-obstacle squares (
targetMask) and end at the end square (endIndex). - The result is
dp[targetMask][endIndex].
- After filling the DP table, the final answer is the number of paths that visit all non-obstacle squares (
Walkthrough
Algorithm
-
State Definition: We define a DP table,
dp[mask][i], which stores the number of paths that visit the set of squares represented bymaskand end at squarei. We can flatten the 2D grid coordinates(r, c)into a single indexi = r * n + c. -
Initialization:
- First, iterate through the grid to find the start
(sr, sc)and end(er, ec)coordinates, and also to create atargetMask. ThetargetMaskwill have a bit set to 1 for every non-obstacle square (0,1, or2). - Convert the start coordinates to a start index
startIndex = sr * n + scand end coordinates toendIndex. - Initialize the DP table:
dp[1 << startIndex][startIndex] = 1. This signifies that there is one path (of length 1) that has visited only the start square and ends at the start square.
- First, iterate through the grid to find the start
-
Transitions:
- Iterate through all possible masks from
1to(1 << (m*n)) - 1. - For each
mask, iterate through each squareufrom0tom*n - 1. - If
dp[mask][u] > 0and squareuis part of the currentmask, it means we have valid paths ending at squareu. - From square
u, try to move to its 4-directional neighborsv. - If neighbor
vis a valid, non-obstacle square and has not been visited yet in the currentmask(i.e.,(mask & (1 << v)) == 0), we can extend the path. Create anewMask = mask | (1 << v). - Update the DP table for the new state:
dp[newMask][v] += dp[mask][u].
- Iterate through all possible masks from
-
Final Result: After filling the DP table, the final answer is the number of paths that visit all non-obstacle squares (
targetMask) and end at the end square (endIndex). The result isdp[targetMask][endIndex].
Code Snippet
class Solution { public int uniquePathsIII(int[][] grid) { int m = grid.length; int n = grid[0].length; int N = m * n; int startIdx = -1, endIdx = -1; int targetMask = 0; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { int idx = i * n + j; if (grid[i][j] != -1) { targetMask |= (1 << idx); } if (grid[i][j] == 1) { startIdx = idx; } else if (grid[i][j] == 2) { endIdx = idx; } } } int[][] dp = new int[1 << N][N]; dp[1 << startIdx][startIdx] = 1; for (int mask = 1; mask < (1 << N); mask++) { for (int u = 0; u < N; u++) { // Check if u is in the current path represented by mask if ((mask & (1 << u)) != 0) { if (dp[mask][u] > 0) { int r = u / n; int c = u % n; int[] dr = {-1, 1, 0, 0}; int[] dc = {0, 0, -1, 1}; for (int i = 0; i < 4; i++) { int nr = r + dr[i]; int nc = c + dc[i]; if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] != -1) { int v = nr * n + nc; // If neighbor v is not yet visited in the current mask if ((mask & (1 << v)) == 0) { int nextMask = mask | (1 << v); dp[nextMask][v] += dp[mask][u]; } } } } } } } return dp[targetMask][endIdx]; }}Complexity
Time
O(2^(m*n) * (m*n)^2) - A more careful analysis shows the loops are over `mask` (2^N), `u` (N), and its neighbors (4), and for each valid transition, we update `dp[nextMask][v]`. A simpler upper bound is often stated as `O(2^N * N^2)`. For each of the `2^N * N` states, we iterate through `N` possible previous states. However, a tighter analysis considering transitions from a state gives `O(2^N * N)`. Let's consider the loops: `mask` (2^N), `u` (N), neighbors (4). This gives `O(2^N * N)`. The provided code implements this tighter complexity.
Space
O(2^(m*n) * (m*n)) - We need to store the DP table, which has `2^N` rows and `N` columns, where `N = m*n`. This can consume a significant amount of memory.
Trade-offs
Pros
Significantly faster than backtracking for the given constraints due to a better time complexity.
Avoids re-computation by memoizing results of subproblems (current location and set of visited cells).
Cons
High memory consumption due to the large DP table, which might be an issue if the constraints were larger.
More complex to conceptualize and implement compared to the straightforward backtracking approach.
Solutions
Solution
class Solution {private int m;private int n;private int cnt;private int[][] grid;private boolean[][] vis;public int uniquePathsIII(int[][] grid) { m = grid.length; n = grid[0].length; this.grid = grid; int x = 0, y = 0; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == 0) { ++cnt; } else if (grid[i][j] == 1) { x = i; y = j; } } } vis = new boolean[m][n]; vis[x][y] = true; return dfs(x, y, 0); }private int dfs(int i, int j, int k) { if (grid[i][j] == 2) { return k == cnt + 1 ? 1 : 0; } int ans = 0; int[] dirs = {-1, 0, 1, 0, -1}; for (int h = 0; h < 4; ++h) { int x = i + dirs[h], y = j + dirs[h + 1]; if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && grid[x][y] != -1) { vis[x][y] = true; ans += dfs(x, y, k + 1); vis[x][y] = false; } } return ans; }}/** * @param {number[][]} grid * @return {number} */ var uniquePathsIII = function (grid) { const m = grid.length; const n = grid[0].length; let [x, y] = [0, 0]; let cnt = 0; for (let i = 0; i < m; ++i) { for (let j = 0; j < n; ++j) { if (grid[i][j] === 0) { ++cnt; } else if (grid[i][j] === 1) { [x, y] = [i, j]; } } } const vis = Array.from({ length: m }, () => Array(n).fill(false)); vis[x][y] = true; const dirs = [-1, 0, 1, 0, -1]; const dfs = function (i, j, k) { if (grid[i][j] === 2) { return k === cnt + 1 ? 1 : 0; } let ans = 0; for (let d = 0; d < 4; ++d) { const x = i + dirs[d]; const y = j + dirs[d + 1]; if ( x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && grid[x][y] !== -1 ) { vis[x][y] = true; ans += dfs(x, y, k + 1); vis[x][y] = false; } } return ans; }; return dfs(x, y, 0); };class Solution {public: int uniquePathsIII(vector<vector<int>> &grid) { int m = grid.size(), n = grid[0].size(); int cnt = 0; for (auto &row : grid) { for (auto &x : row) { cnt += x == 0; } } int dirs[5] = {-1, 0, 1, 0, -1}; bool vis[m][n]; memset(vis, false, sizeof vis); function<int(int, int, int)> dfs = [&](int i, int j, int k) -> int { if (grid[i][j] == 2) { return k == cnt + 1 ? 1 : 0; } int ans = 0; for (int h = 0; h < 4; ++h) { int x = i + dirs[h], y = j + dirs[h + 1]; if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && grid[x][y] != -1) { vis[x][y] = true; ans += dfs(x, y, k + 1); vis[x][y] = false; } } return ans; }; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == 1) { vis[i][j] = true; return dfs(i, j, 0); } } } return 0; }};''' >>> ( (i,j) for i in range(3) for j in range(3) if i == j ) <generator object <genexpr> at 0x104da5150> >>> list( (i,j) for i in range(3) for j in range(3) if i == j ) [(0, 0), (1, 1), (2, 2)] >>> next( (i,j) for i in range(3) for j in range(3) if i == j ) (0, 0) basically next() is same as list()[0] ''' class Solution : # dfs def uniquePathsIII ( self , grid : List [ List [ int ]]) -> int : def dfs ( i , j , k ): if grid [ i ][ j ] == 2 : return int ( k == cnt + 1 ) ans = 0 for a , b in pairwise ( dirs ): x , y = i + a , j + b if 0 <= x < m and 0 <= y < n and ( x , y ) not in vis and grid [ x ][ y ] != - 1 : vis . add (( x , y )) ans += dfs ( x , y , k + 1 ) vis . remove (( x , y )) # set() cannot pop(index) or pop(val), only pop() return ans m , n = len ( grid ), len ( grid [ 0 ]) start = next (( i , j ) for i in range ( m ) for j in range ( n ) if grid [ i ][ j ] == 1 ) dirs = ( - 1 , 0 , 1 , 0 , - 1 ) cnt = sum ( grid [ i ][ j ] == 0 for i in range ( m ) for j in range ( n )) vis = { start } # start here is a tuple (i,j) return dfs ( * start , 0 ) # '*start' means that the elements of the start iterable are being unpacked # and passed as separate arguments to the dfs function ######## ''' >>> visited = set([(1,1),(2,2)]) >>> visited {(1, 1), (2, 2)} >>> >>> visited | {(3,3), (4,4)} {(4, 4), (1, 1), (3, 3), (2, 2)} ''' # 980. Unique Paths III # https://leetcode.com/problems/unique-paths-iii/ class Solution : # bfs def uniquePathsIII ( self , grid : List [ List [ int ]]) -> int : rows , cols = len ( grid ), len ( grid [ 0 ]) available = 0 sx = sy = ex = ey = 0 # start x/y, end x/y res = 0 for x in range ( rows ): for y in range ( cols ): if grid [ x ][ y ] != - 1 : available += 1 if grid [ x ][ y ] == 1 : sx , sy = x , y elif grid [ x ][ y ] == 2 : ex , ey = x , y queue = deque ([( sx , sy , 1 , set ([( sx , sy )]))]) # x, y, count, visited while queue : x , y , count , visited = queue . popleft () if x == ex and y == ey and count == available : res += 1 continue for dx , dy in [( x + 1 , y ), ( x - 1 , y ), ( x , y + 1 ), ( x , y - 1 )]: if 0 <= dx < rows and 0 <= dy < cols and grid [ dx ][ dy ] != - 1 and count + 1 <= available and ( dx , dy ) not in visited : new_visited = visited | {( dx , dy )} # or just add() queue . append (( dx , dy , count + 1 , new_visited )) return resVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.