top of page

Unique Paths

  • 22 hours ago
  • 7 min read

How to Think Through a Grid DP Problem From Scratch


Unique Paths tests whether you can recognize a counting dynamic programming pattern and model state correctly. It's a classic interview question because the logic scales cleanly to harder grid problems. Once you've internalized the thinking here, a whole family of harder problems opens up.


Problem Statement

A robot is located at the top-left corner of an m × n grid.

The robot can only move either down or right at any point in time.

The robot is trying to reach the bottom-right corner of the grid.

Return the number of unique paths the robot can take to reach the destination.


1. Clarify Requirements Before Jumping Into Code

Before writing anything, let's make sure the rules are clear.

Input: Two integers m (rows) and n (columns)

Output: Number of unique paths from (0, 0) to (m-1, n-1)

Allowed moves: Right, Down

Quick edge checks:

  • What if m = 1 or n = 1? There's only one path, so you can only move in one direction.

  • No obstacles in this version.

  • The robot must stay inside the grid.

Getting these clarifications out loud in an interview signals that you think carefully before you code.


2. Identify the Category of the Problem

This is a counting problem on a grid. When you see those two things together, Dynamic Programming should immediately come to mind.


The key insight: we're not looking for which path is best, we're counting how many paths exist. That's an additive structure, not an optimization. Greedy won't work here, and sorting won't help either. What we need is a way to systematically count without re-doing work.

That's DP.


3. Start With Brute Force to Build Intuition

The most natural first instinct is recursion. At every cell, the robot has at most two choices: go right or go down. So you explore both branches and sum the results.

int countPaths(int i, int j, int m, int n) {
    if (i == m - 1 && j == n - 1) return 1;  // reached destination
    if (i >= m || j >= n) return 0;         // out of bounds

    return countPaths(i + 1, j, m, n) + countPaths(i, j + 1, m, n);
}

This works correctly. But let's think about what it's doing.


For a 3×7 grid, the recursion tree explodes. The same subproblem - "how many paths from cell (2, 3)?" - gets computed dozens of times. You're doing redundant work on a massive scale.


Time complexity: Exponential — roughly O(2^(m+n))

Space complexity: O(m + n) recursion depth


This is too slow, but it shows us something important: the subproblems overlap. That's the exact condition under which memoization or DP pays off.


4. Brainstorm More Solutions

Step 1: Memoization - Fix the Brute Force

The simplest improvement is to cache results. If we've already computed "how many paths from (i, j)?", we store it so we don't have to recompute.


int countPaths(int i, int j, int m, int n, int[][] memo) {
    if (i == m - 1 && j == n - 1) return 1;
    if (i >= m || j >= n) return 0;
    if (memo[i][j] != 0) return memo[i][j];

    memo[i][j] = countPaths(i + 1, j, m, n, memo)
               + countPaths(i, j + 1, m, n, memo);
    return memo[i][j];
}

Now each cell is computed exactly once. Time complexity drops to O(m × n), while space complexity is O(m × n) for the memo table plus O(m + n) for the call stack.


This is a solid solution. But let's think about whether we can do better. Can we understand the problem more deeply?


Step 2: Flip the Perspective - Bottom-Up 2D DP

Memoization is top-down: we start at the destination and work backward. But with memoizaiton and DP, there's usually an alternative: start at the origin and build forward.


Let's ask ourself the question: for any cell (i, j), where could the robot have come from?


There are only two possibilities:

  • From the cell directly above: (i-1, j)

  • From the cell directly to the left: (i, j-1)


That gives us a clean recurrence:

dp[i][j] = dp[i-1][j] + dp[i][j-1]

Now we need some base cases. Are there any cells that where the answer is obvious? We know there's only one path for the start cell, so we can fill in 1 for cell dp[0][0]. Can we extend that?

Let's look at the whole first row: immediately we see that every cell in the top row can only be reached by moving right from the start, which means there's exactly one way to reach each of them. The same logic applies for the first column - you can only move down.


So our base cases are:

  • dp[0][j] = 1 for all j (entire first row)

  • dp[i][0] = 1 for all i (entire first column)


Now to confirm our logic, let's trace through a 3×3 grid to make this concrete:

Initial (base cases):
1  1  1
1  0  0
1  0  0

Fill in row by row:
dp[1][1] = dp[0][1] + dp[1][0] = 1 + 1 = 2
dp[1][2] = dp[0][2] + dp[1][1] = 1 + 2 = 3

dp[2][1] = dp[1][1] + dp[2][0] = 2 + 1 = 3
dp[2][2] = dp[1][2] + dp[2][1] = 3 + 3 = 6

Final grid:
1  1  1
1  2  3
1  3  6

Answer: 6. We can manually verify this is correct for a 3×3 grid.

Time: O(m × n).

Space: O(m × n).


Step 3: Notice the Redundancy - 1D Space Optimization

Now let's think about optimization. For a DP question, that often means asking ourselves: do we really need the whole DP array/grid? Or can we get away with storing fewer variables?


Let's look at the recurrence again:

dp[i][j] = dp[i-1][j] + dp[i][j-1]

When computing row i, we only ever look at two spots: row i-1 (directly above) and cell immediately to the left of the current row. We never look at rows i-2, i-3, etc.


That means we don't need to store the entire grid. We only need one row at a time.

Here's the key insight: if we process cells left-to-right and update in place, then at the moment we compute dp[j]:

  • dp[j]  still holds the value from the previous row (the "from above" contribution)

  • dp[j-1]  has already been updated for the current row (the "from left" contribution)


So, dp[j] = dp[j] + dp[j-1] allows us to compute our answer with no extra storage needed.


Let's trace this for the same 3×3:

Start: dp = [1, 1, 1]   ← base case: all 1s

Row 1 (i=1):
  j=1: dp[1] = dp[1] + dp[0] = 1 + 1 = 2  →  dp = [1, 2, 1]
  j=2: dp[2] = dp[2] + dp[1] = 1 + 2 = 3  →  dp = [1, 2, 3]

Row 2 (i=2):
  j=1: dp[1] = dp[1] + dp[0] = 2 + 1 = 3  →  dp = [1, 3, 3]
  j=2: dp[2] = dp[2] + dp[1] = 3 + 3 = 6  →  dp = [1, 3, 6]

Answer: dp[2] = 6 ✓

Time: O(m × n).

Space: O(n). Same speed, a fraction of the memory.


Step 4: Is There a Mathematical Solution?

Mathematical solutions are elegant but difficult to identify in an interview setting. For this question, there is a mathematical solution, and it's worth knowing about even if you wouldn't actually reach for it first in an interview.


Think about what any valid path looks like. The robot always makes exactly (m-1) moves down and (n-1) moves right, for a total of (m+n-2) moves. The question becomes: in how many ways can you arrange those moves?


That's a combinations problem: choose which (m-1) of the (m+n-2) total moves will be "down." The rest are automatically "right."

Answer = C(m+n-2, m-1) = (m+n-2)! / ((m-1)! * (n-1)!)

For the 3×3 case: C(4, 2) = 6.


This runs in O(m+n) time and O(1) space. In an interview, you'd likely want to mention this after presenting the DP solution. It demonstrates mathematical depth and shows you're thinking beyond the standard template. But you still want to demonstrate your ability to think through the algorithmic solutions first.


5. Trade-Offs at a Glance

Approach

Time

Space

Notes

Brute-force recursion

O(2^(m+n))

O(m+n)

Too slow; good for building intuition

Memoized recursion

O(m × n)

O(m × n)

Intuitive; has call stack overhead

2D bottom-up DP

O(m × n)

O(m × n)

Clean and easy to trace

1D optimized DP

O(m × n)

O(n)

Best general solution

Combinatorics

O(m+n)

O(1)

Elegant; risk of overflow on large inputs

In an interview, implement the 1D DP. Mention the combinatorics solution as a bonus.


6. Write Pseudocode First

initialize dp array of size n, all set to 1

for each row i from 1 to m-1:
    for each column j from 1 to n-1:
        dp[j] = dp[j] + dp[j-1]

return dp[n-1]

This captures the entire algorithm in five lines. Get comfortable with it before writing real code.


7. Consider Edge Cases

Think through these before and after writing your solution:

  • m = 1 or n = 1: Only one path exists. The DP handles this already. The inner loop never executes, and dp[n-1] stays 1.

  • m = 1, n = 1: One cell, one "path" (do nothing). Our solution returns 1.

  • Large inputs: m = 100, n = 100, which means the answer is astronomically large. For the combinatorics approach, you'd need to handle overflow carefully. The DP approach is fine in Java since the problem constraints are usually small enough for int variables.


8. Full Solution

public class UniquePaths {

    public static int uniquePaths(int m, int n) {
        int[] dp = new int[n];

        // Base case: first row is all 1s
        for (int j = 0; j < n; j++) {
            dp[j] = 1;
        }

        // Fill remaining rows
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                // dp[j] = (paths from above) + (paths from left)
                dp[j] = dp[j] + dp[j - 1];
            }
        }

        return dp[n - 1];
    }
}

9. Test Your Solution

System.out.println(uniquePaths(1, 1));   // 1  — single cell
System.out.println(uniquePaths(1, 5));   // 1  — single row
System.out.println(uniquePaths(5, 1));   // 1  — single column
System.out.println(uniquePaths(2, 2));   // 2  — simplest non-trivial case
System.out.println(uniquePaths(3, 3));   // 6  — traced manually above
System.out.println(uniquePaths(3, 7));   // 28
System.out.println(uniquePaths(10, 10)); // 48620

10. Key Lessons for Future Problems

  • Ask where each state comes from. In this problem, every cell can only be reached from above or from the left. That single observation unlocks the entire solution.

  • Counting is additive, not greedy. Whenever a problem asks "how many ways," your instinct should be: define a state, find its recurrence, add up contributions. Don't try to greedily pick one path.

  • Memoization and bottom-up DP solve the same problem differently. Memoization is easier to derive (just cache the recursion). Bottom-up is often cleaner and avoids stack overhead. Know both.

  • Look for space optimization once the recurrence is clear. If row i only depends on row i-1, you can compress to one row. This pattern appears constantly.

  • Know the mathematical alternative. Many counting problems on grids have a closed-form combinatorics solution. It won't always apply, but recognizing when it does signals strong fundamentals.


Unique Paths is less about the robot and more about learning to think in grids, a skill that compounds across dozens of interview problems.


Good Luck and Happy Coding!

Recent Posts

See All
Target Sum

Learn how to solve the Target Sum coding problem to prepare for your next technical interview!

 
 
 
Coin Change

Learn how to solve the Coin Change coding problem to prepare for your next technical interview!

 
 
 
House Robber

Learn how to solve the House Robber coding problem to prepare for your next technical interview!

 
 
 

Comments


Drop Me a Line, Let Me Know What You Think

Thanks for submitting!

© 2023 by Train of Thoughts. Proudly created with Wix.com

bottom of page