62. Unique Paths - Explanation
Description
There is an m x n grid where you are allowed to move either down or to the right at any point in time.
Given the two integers m and n, return the number of possible unique paths that can be taken from the top-left corner of the grid (grid[0][0]) to the bottom-right corner (grid[m - 1][n - 1]).
You may assume the output will fit in a 32-bit integer.
Example 1:
Input: m = 3, n = 6
Output: 21Example 2:
Input: m = 3, n = 3
Output: 6Constraints:
1 <= m, n <= 100
Topics
Recommended Time & Space Complexity
You should aim for a solution as good or better than O(m * n) time and O(m * n) space, where m is the number of rows and n is the number of columns in the grid.
Hint 1
Try to think in terms of recursion and visualize it as a decision tree, where we have two choices at each step. Can you determine the base condition and recurrence relation?
Hint 2
We recursively traverse the grid using row i and column j. At each step, we explore both possibilities: moving down or moving right, ensuring we do not go out of bounds. If we reach the bottom-right cell, we return 1.
Hint 3
This approach has exponential complexity. Can you think of a way to optimize the recursion? Maybe you should consider using a dynamic programming approach.
Hint 4
We can use memoization to cache the results of recursive calls and avoid recalculations. A hash map or a 2D array can be used to store these results.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - Breaking down the problem into smaller subproblems (paths from adjacent cells)
- Dynamic Programming (Memoization) - Caching computed results to avoid redundant calculations
- Dynamic Programming (Tabulation) - Building solutions bottom-up using a DP table
- Combinatorics - Understanding that path counting is equivalent to choosing positions for moves
1. Recursion
Intuition
This is the pure recursive (brute force) way to think about the problem.
From any cell (i, j) in the grid:
- You can only move right or down.
- The total number of paths from
(i, j)is:paths going right + paths going down
Base ideas:
- If you reach the bottom-right cell, you found one valid path.
- If you go out of bounds, that path is invalid (count = 0).
So the problem naturally breaks into smaller subproblems, making recursion a direct fit.
Algorithm
- Start from the top-left cell
(0, 0). - At each cell
(i, j):- If
(i, j)is the destination(m-1, n-1), return1. - If
(i, j)is outside the grid, return0.
- If
- Recursively compute:
- Paths by moving right ->
(i, j + 1) - Paths by moving down ->
(i + 1, j)
- Paths by moving right ->
- Return the sum of both.
- The answer is the result from
(0, 0).
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows and is the number of columns.
2. Dynamic Programming (Top-Down)
Intuition
This is the optimized version of recursion using memoization.
In the brute-force approach, the same cell (i, j) is solved many times.
But the number of paths from a cell never changes, so we can store it once and reuse it.
Think of it this way:
- Every cell
(i, j)asks:
“How many ways can I reach the destination from here?” - Once answered, we cache it so we never recompute it.
This turns an exponential recursion into a polynomial-time solution.
Algorithm
- Create a 2D memo table
memo[m][n], initialized to-1. - Define a recursive function
dfs(i, j):- If
(i, j)is the destination(m-1, n-1), return1. - If
(i, j)is out of bounds, return0. - If
memo[i][j]is already computed, return it.
- If
- Otherwise:
- Compute paths by moving right and down:
memo[i][j] = dfs(i, j+1) + dfs(i+1, j)
- Compute paths by moving right and down:
- Return
memo[i][j]. - Start recursion from
(0, 0).
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
memo = [[-1] * n for _ in range(m)]
def dfs(i, j):
if i == (m - 1) and j == (n - 1):
return 1
if i >= m or j >= n:
return 0
if memo[i][j] != -1:
return memo[i][j]
memo[i][j] = dfs(i, j + 1) + dfs(i + 1, j)
return memo[i][j]
return dfs(0, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows and is the number of columns.
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of starting from the top and recursing, we build the answer from the destination backward.
From any cell (i, j), the number of unique paths to the destination is:
- paths from the cell below
(i+1, j) - plus paths from the cell to the right
(i, j+1)
If we already know these values, we can compute the current cell directly.
So we:
- Set the destination cell to
1 - Fill the grid bottom-up, right-to-left
Algorithm
- Create a
(m+1) x (n+1)DP table initialized with0. - Set
dp[m-1][n-1] = 1(only one way to stay at destination). - Traverse rows from
m-1to0and columns fromn-1to0. - For each cell
(i, j):dp[i][j] = dp[i+1][j] + dp[i][j+1] - Return
dp[0][0].
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows and is the number of columns.
4. Dynamic Programming (Space Optimized)
Intuition
Each cell only depends on:
- the cell to the right (same row)
- the cell below (previous row)
So instead of storing the entire 2D grid, we can keep just one row at a time.
We update the row from right to left, using values from:
- the current row (
newRow[j + 1]) - the previous row (
row[j])
This reduces space while keeping the same logic.
Algorithm
- Initialize a 1D array
rowof sizenwith all1s
(only one way to move right along the bottom row). - Repeat for
m - 1rows:- Create a new row filled with
1s. - Traverse columns from right to left (excluding last column).
- Update:
newRow[j] = newRow[j + 1] + row[j] - Replace
rowwithnewRow.
- Create a new row filled with
- Return
row[0](top-left cell).
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows and is the number of columns.
5. Dynamic Programming (Optimal)
Intuition
From any cell, you can reach the destination by moving:
- right
- down
The number of ways to reach a cell equals:
ways from the cell below + ways from the cell to the right
Instead of using a full 2D table, we notice that:
- each row only depends on the row below it
- so a single 1D array is enough
We keep updating this array from right to left, accumulating paths.
Algorithm
- Initialize a 1D array
dpof sizenwith all values as1
(only one way along the last row). - For each remaining row (from bottom to top):
- Traverse columns from right to left (excluding last column).
- Update:
dp[j] = dp[j] + dp[j + 1]
- After all rows are processed,
dp[0]contains the total number of unique paths. - Return
dp[0].
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows and is the number of columns.
6. Math
Intuition
To go from the top-left to the bottom-right of an m x n grid, you can only move right or down.
- You must move down (m - 1) times
- You must move right (n - 1) times
So overall, you make (m + n - 2) moves.
The problem becomes:
In how many different ways can we arrange these right and down moves?
This is a combinations problem:
- Choose positions for the right moves (or down moves)
That gives:
Algorithm
- If either
mornis1, return1(only one path). - Always use the smaller value between
mandnto reduce calculations. - Compute the combination value iteratively (without factorials).
- Return the final result.
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows and is the number of columns.
Common Pitfalls
Confusing Rows and Columns with Moves
Misunderstanding that an m x n grid requires m - 1 down moves and n - 1 right moves (not m and n). The total moves is (m - 1) + (n - 1) = m + n - 2.
Wrong Base Case in Recursion
Returning 1 when reaching any boundary instead of only the destination cell. The base case should trigger only at (m-1, n-1), not when hitting the last row or column.
# Wrong: counts incomplete paths
if i == m - 1 or j == n - 1:
return 1
# Correct: only count at destination
if i == m - 1 and j == n - 1:
return 1Integer Overflow in Math Solution
When computing combinations for larger grids, intermediate multiplication can overflow. Use long types and divide as you multiply to keep values manageable.
// Risk of overflow
res = factorial(m + n - 2) / (factorial(m - 1) * factorial(n - 1));
// Safer: multiply and divide iteratively
res *= i;
res /= j;
Sign in to join the discussion