Unique Paths III is a classic backtracking problem. It looks like a grid-walk question, but the real lesson is the mark / recurse / un-mark rhythm that every backtracking solution shares.
Problem. You are given a grid where 1 is the start, 2 is the end, 0 is an empty square
you may walk on, and -1 is an obstacle. Return the number of distinct paths from start to end that
walk over every non-obstacle square exactly once.
Example: the 3x3 grid [[1,0,0],[0,0,0],[2,0,0]] has exactly 1 such path (a snake that covers all 9
squares and finishes on the end square).
The slow way first
There is no clever shortcut here: we genuinely have to try paths. The naive worry is that we might revisit squares or wander forever. The fix is to track which squares are on the current path and refuse to step on one twice. That turns an infinite walk into a finite tree of choices we can search exhaustively.
The question to ask: how do I try a square, explore everything that follows, and then cleanly undo my choice so I can try the next one? That is exactly what backtracking gives us.
The idea: count, then DFS with un-marking
First count how many squares we must cover (every 0, plus the start). Then run a depth-first search
from the start. At each square we mark it visited, recurse into its unvisited neighbours, and then
un-mark it on the way out so other paths can use it. A path scores 1 only when we land on the end
square having covered all squares.
The key insight: the un-mark step is what makes one DFS explore many paths instead of just one. Without it, a square used by an early dead-end would stay blocked forever.
Walk through it
Step through the animation. The square we are standing on glows; squares already on the path turn visited.
We snake through all 9 squares and finish on the end square 2. When a branch hits an already-visited
neighbour it is blocked and backtracks. Landing on the end with every square covered counts as one path.
Pseudocode
empty = count of 0-squares + 1 (the start)
find the start square
dfs(r, c, remaining):
mark (r, c) visited
remaining -= 1
if (r, c) is the end square:
return 1 if remaining == 0 else 0 # only valid if all covered
total = 0
for each in-bounds neighbour (nr, nc):
if (nr, nc) is not visited / not an obstacle:
total += dfs(nr, nc, remaining)
un-mark (r, c) # backtrack
return total
answer = dfs(start, empty)The Python solution
def unique_paths(grid):
empty = sum(row.count(0) for row in grid) + 1
start = find(grid, 1)
def dfs(r, c, remaining):
grid[r][c] = -1 # mark visited
remaining -= 1
if grid_was_end(r, c):
return 1 if remaining == 0 else 0
total = 0
for nr, nc in neighbours(r, c):
if grid[nr][nc] != -1:
total += dfs(nr, nc, remaining)
grid[r][c] = 0 # un-mark (backtrack)
return total
return dfs(*start, empty)emptyis the number of squares we must cover: every0plus the start square itself.startis the(row, col)of the1square; the search begins there.- Line 6 marks the current square by overwriting it with
-1(the same value as an obstacle), so neighbours treat it as off-limits. - Line 9 is the scoring line: we only return
1when we are on the end square andremaining == 0, meaning every square has been covered. - The loop tries each neighbour that is not an obstacle or already visited, summing the paths each yields.
- Line 14 is the heart of backtracking — we un-mark the square back to
0before returning, freeing it for other paths.
Complexity
| Case | Time | Notes |
|---|---|---|
| Search every path | O(4^(R·C)) (moderate) | up to 4 choices per square |
| Pruned by visited marks | much smaller in practice (moderate) | blocked squares cut branches |
O(R·C) (moderate)The branching factor is at most 4 (the four directions), and the grid has R·C squares, so the worst case
is exponential. The visited-marking prunes hard in practice, but the upper bound stays exponential — typical
for exhaustive backtracking. Space is O(R·C) for the recursion depth.
When this pattern shows up
Whenever a problem asks for all arrangements, paths, or combinations under constraints — permutations, N-Queens, word search, Sudoku — reach for backtracking. The skeleton is always the same: choose, recurse, un-choose. Unique Paths III is that skeleton applied to a grid walk.
Do not forget to un-mark on the way out. If you mark a square and return without restoring it, every later path that needed that square is silently broken — a bug that is easy to write and hard to spot.
Practice
On the 3x3 grid [[1,0,0],[0,0,0],[2,0,0]], why does a path that reaches the end after only 5 squares score 0?
1. What makes one DFS explore many different paths instead of just one?
2. When does a path count toward the answer?
3. Why do we mark a visited square with -1?
4. Why is the worst-case time exponential?