Pattern

Dynamic Programming

Break complex problems into overlapping subproblems and cache results to avoid redundant computation.

Roadmap

  1. What Is This Pattern?
  2. When to Use It
  3. The 5-Step Framework
  4. Top-Down vs Bottom-Up
  5. Common DP Patterns
  6. Walkthrough: Coin Change
  7. Space Optimization
  8. Common Problems
  9. Interactive Demo
  10. Complexity Analysis
  11. Key Takeaways
Step 01

What Is This Pattern?

Dynamic Programming (DP) is an optimization technique that solves complex problems by breaking them into smaller subproblems, solving each subproblem only once, and storing the result. A problem is a candidate for DP when it exhibits two key properties:

The Two Properties

OPTIMAL SUBSTRUCTURE
The optimal solution to the problem can be constructed from optimal solutions to its subproblems. If you know the best answer for smaller inputs, you can combine them to get the best answer for the full input.
OVERLAPPING SUBPROBLEMS
The same subproblems are solved multiple times during recursion. Without caching, you waste time recomputing answers you already found. DP stores them so each subproblem is solved exactly once.

Let's see this with Fibonacci. A naive recursion tree for fib(5) recomputes the same values many times:

Naive Recursion Tree (Repeated Work)

f(5) f(4) f(3) f(3) f(2) f(2) f(1) f(2) f(1) f(1) f(0) f(1) f(0)
Recomputed (wasted work)
Base case

f(3) is computed twice, f(2) three times. For fib(n), naive recursion does O(2^n) work.

With DP Caching (Each Value Computed Once)

f(5) =5 f(4) =3 f(3) cache! f(3) =2 f(2) cache! f(2) =1 f(1) =1
Cache hit (instant return)
Dashed = looked up, not recomputed

With memoization, we compute each subproblem once. f(3) and f(2) are returned from cache. Total work: O(n).

💡

DP vs Divide & Conquer: Both break problems into subproblems, but divide & conquer (like merge sort) has non-overlapping subproblems, so caching doesn't help. DP vs Greedy: Greedy makes locally optimal choices and never looks back. DP considers all possible choices at each step, guaranteeing a global optimum.

Step 02

When to Use It

Look for these recognition signals in the problem statement. If you spot one, DP is very likely the intended approach.

"Find the minimum/maximum cost to..."
Optimization over a sequence of decisions. Each decision depends on previous results.
"Count the number of ways to..."
Combinatorial counting where the total is the sum of sub-totals from smaller cases.
"Is it possible to..." (yes/no)
Feasibility problems where you make choices and need to check if any combination works.
"Can you define it in terms of smaller subproblems?"
The ultimate test. If the answer for input n can be computed from answers for inputs < n, DP applies.

Not DP? If the problem has no overlapping subproblems (each sub-problem is unique), use divide & conquer. If a locally optimal choice always leads to a globally optimal solution (e.g., activity selection with greedy), DP is overkill. If the problem asks for all permutations or the actual path, you may need backtracking instead.

Step 03

The 5-Step Framework

Follow these five steps for every DP problem. This structured approach eliminates guesswork and works universally, from 1D arrays to complex multi-dimensional states.

1
Define the State
What does dp[i] (or dp[i][j]) represent? This is the most critical step. The state must capture enough information to make decisions and compute the answer. Example: for Coin Change, dp[i] = minimum coins needed to make amount i.
2
Write the Recurrence
How does dp[i] relate to smaller states? Express the current state in terms of previous states. Example: dp[i] = min(dp[i - coin] + 1) for each valid coin. This is the "transition function."
3
Set Base Cases
What are the trivial answers that don't need further recursion? Example: dp[0] = 0 (zero coins needed to make amount 0). Base cases anchor the recursion and prevent infinite loops.
4
Determine Fill Order
Bottom-up: fill the table so that when you compute dp[i], all states it depends on are already computed. Top-down: let recursion + memoization handle the order automatically. Either approach works; bottom-up avoids stack overhead.
5
Find the Answer
Which cell holds the final answer? Often it's dp[n] or dp[m][n], but sometimes you need max(dp[...]) across all states. Example: for LIS, the answer is max(dp[0..n-1]), not just dp[n-1].

Applying the Framework: Climbing Stairs

You can climb 1 or 2 steps. How many distinct ways to reach step n?

1. State: dp[i] = number of ways to reach step i
2. Recurrence: dp[i] = dp[i-1] + dp[i-2] (arrive from step i-1 or i-2)
3. Base cases: dp[0] = 1, dp[1] = 1
4. Fill order: Left to right: i = 2, 3, ..., n
5. Answer: dp[n]
1
1
2
3
5
8
dp[0]  dp[1]  dp[2]  dp[3]  dp[4]  dp[5]
8 ways to climb 5 stairs
Step 04

Top-Down vs Bottom-Up

There are two ways to implement every DP solution. Both produce identical results. Let's compare them side by side using Fibonacci.

Top-Down (Memoization)

Map<Integer,Integer> memo = new HashMap<>();

int fib(int n) {
    if (n <= 1) return n;
    if (memo.containsKey(n))
        return memo.get(n);

    int res = fib(n-1) + fib(n-2);
    memo.put(n, res);
    return res;
}
+ Natural recursive thinking
+ Only computes needed states
- Recursion stack overhead
- Harder to optimize space

Bottom-Up (Tabulation)

int fib(int n) {
    if (n <= 1) return n;
    int[] dp = new int[n + 1];
    dp[0] = 0;
    dp[1] = 1;

    for (int i = 2; i <= n; i++)
        dp[i] = dp[i-1] + dp[i-2];

    return dp[n];
}
+ No recursion overhead
+ Easy to optimize space
- Must determine fill order
- Computes all states even if unused
🎯

Which to choose? Start with top-down (memoization) when figuring out a problem; it maps directly to the recurrence. Then convert to bottom-up for interviews, as it is generally faster in practice and easier to space-optimize. For most LeetCode problems, bottom-up is the expected approach.

Step 05

Common DP Patterns

Most DP problems fall into one of these four categories. Recognizing which pattern applies immediately tells you the state definition and recurrence shape.

1D DP

Climbing Stairs, House Robber

dp[i] depends on dp[i-1] and/or dp[i-2]. Single array, linear scan.

// House Robber: dp[i] = max money robbing houses 0..i
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
dp[0]
dp[1]
dp[2]
dp[3]
dp[n]
2D DP

Unique Paths, Edit Distance

dp[i][j] depends on neighboring cells. Grid traversal, fill row by row.

// Unique Paths: dp[i][j] = paths to cell (i,j)
dp[i][j] = dp[i-1][j] + dp[i][j-1];
1
1
1
1
1
1
2
3
4
5
1
3
6
10
15

3x5 grid: 15 unique paths to bottom-right

Knapsack

0/1 Knapsack, Coin Change

At each item, choose to include or exclude it. The recurrence branches into two choices.

// 0/1 Knapsack: dp[i][w] = max value using items 0..i with capacity w
dp[i][w] = Math.max(
    dp[i-1][w],                    // skip item i
    dp[i-1][w - wt[i]] + val[i]    // take item i
);
Skip item
dp[i-1][w]
vs
Take item
dp[i-1][w-wt]+val
Best of both
dp[i][w]
String DP

LCS, Palindromic Subsequence

Two strings mapped to a 2D grid. Diagonal, left, and up transitions for match/mismatch.

// LCS: dp[i][j] = LCS length of s1[0..i-1] and s2[0..j-1]
if (s1[i-1] == s2[j-1])
    dp[i][j] = dp[i-1][j-1] + 1;     // match: diagonal + 1
else
    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); // mismatch: max of skip either
a b c d e
0 0 0 0 0 0
a 0 1 1 1 1 1
c 0 1 1 2 2 2
e 0 1 1 2 2 3

LCS("ace", "abcde") = 3

Step 06

Walkthrough: Coin Change

Given coins = [1, 2, 5] and amount = 11, find the minimum number of coins to make that amount. Let's apply the 5-step framework and watch the DP table fill.

The Framework Applied

1. State: dp[i] = minimum coins to make amount i
2. Recurrence: dp[i] = min(dp[i - c] + 1) for each coin c where i - c ≥ 0
3. Base case: dp[0] = 0 (0 coins for amount 0)
4. Fill order: Left to right: i = 1, 2, ..., 11
5. Answer: dp[11]

DP Table: coins = [1, 2, 5], amount = 11

Each cell shows the minimum coins needed. The small label shows which coin was used last.

i 01234567891011
dp[i] 0
1
+1
1
+2
2
+1
2
+2
1
+5
2
+1
2
+2
3
+1
3
+2
2
+5
3
+1
dp[5] = 1 (use one 5-coin: 5)
dp[10] = 2 (two 5-coins: 5+5)
dp[11] = 3 (dp[10]+1 = 5+5+1)

Full Annotated Code

int coinChange(int[] coins, int amount) {
    int[] dp = new int[amount + 1];
    Arrays.fill(dp, amount + 1);  // "infinity" sentinel
    dp[0] = 0;                     // base case

    for (int i = 1; i <= amount; i++) {
        for (int coin : coins) {
            if (coin <= i) {
                dp[i] = Math.min(dp[i], dp[i - coin] + 1);
            }
        }
    }

    return dp[amount] > amount ? -1 : dp[amount];
}
💡

Tracing the solution: dp[11] = 3 comes from dp[10] + 1 (coin 1). dp[10] = 2 comes from dp[5] + 1 (coin 5). dp[5] = 1 comes from dp[0] + 1 (coin 5). So the coins used are: 5 + 5 + 1 = 11.

Step 07

Space Optimization

Many DP problems use an entire table but only reference the previous row (or previous few values). You can reduce space by keeping only what you need.

The Rolling Array Technique

If dp[i] only depends on dp[i-1] and dp[i-2], you don't need the full array. Just keep two variables.

Before: O(n) Space

int[] dp = new int[n + 1];
dp[0] = 1; dp[1] = 1;
for (int i = 2; i <= n; i++)
    dp[i] = dp[i-1] + dp[i-2];
return dp[n];

After: O(1) Space

int prev2 = 1, prev1 = 1;
for (int i = 2; i <= n; i++) {
    int curr = prev1 + prev2;
    prev2 = prev1;
    prev1 = curr;
}
return prev1;

2D to 1D: Row Compression

For 2D DP where dp[i][j] only depends on row i-1, keep just one row and overwrite in place.

// Unique Paths: O(m*n) space -> O(n) space
int[] dp = new int[n];
Arrays.fill(dp, 1);               // first row: all 1s
for (int i = 1; i < m; i++)
    for (int j = 1; j < n; j++)
        dp[j] += dp[j - 1];          // dp[j] already has "above"
return dp[n - 1];

When can you optimize? Look at which states each cell references. If it only looks at the previous row (or a fixed window of previous values), you can compress. The Coin Change problem already uses 1D because dp[i] references dp[i - coin] in the same array, so it's already optimal at O(amount) space.

Step 08

Common Problems

These are the essential LeetCode problems for mastering dynamic programming. They are grouped by sub-pattern and listed in rough order of difficulty.

#70 Climbing Stairs Easy
#198 House Robber Medium
#322 Coin Change Medium
#300 Longest Increasing Subsequence Medium
#62 Unique Paths Medium
#1143 Longest Common Subsequence Medium
#72 Edit Distance Medium
📝

Practice tip: Start with #70 (Climbing Stairs) to internalize the framework. Then do #198 (House Robber) for the include/exclude pattern. #322 (Coin Change) is the gateway to knapsack-type problems. #1143 (LCS) and #72 (Edit Distance) teach you 2D string DP. Master these seven and you can handle most DP problems on LeetCode.

Step 09

Interactive Demo

Enter coins and a target amount, then step through the Coin Change DP algorithm. Watch each cell fill in and see which coin produced the optimal value.

⚙ Coin Change Visualizer



Press "Step →" or "Run All" to begin.
Step 10

Complexity Analysis

DP complexity varies by problem, but a universal formula applies:

Time = States x Transitions per State
Space = Number of States Stored

Examples

Problem States Transitions Time Space
Climbing Stairs n 2 O(n) O(1)*
Coin Change amount |coins| O(n*k) O(n)
LCS m * n 1 O(mn) O(n)*
Edit Distance m * n 3 O(mn) O(n)*
LIS n n O(n²) O(n)

* with space optimization (rolling array)

📝

Quick estimation: Count the number of unique states (the size of your DP array/table), then multiply by the cost of computing each state. This gives you a tight bound on both time and the number of operations.

Step 11

Key Takeaways

01

The 5-step framework is universal. Define state, write recurrence, set base cases, determine fill order, locate the answer. This works for every single DP problem, from Fibonacci to complex multi-dimensional problems.

02

Defining the state is the hardest part. If you cannot clearly state what dp[i] represents in plain English, the rest of the solution will not follow. Spend the most time here.

03

Start top-down, optimize bottom-up. Memoized recursion lets you think in the natural recursive structure of the problem. Once your solution works, convert to tabulation for better constant-factor performance and space optimization.

04

Recognize the four sub-patterns. 1D DP, 2D grid DP, Knapsack (include/exclude), and String DP cover the vast majority of DP problems. Identifying which one applies narrows your approach immediately.

05

Space optimization is a follow-up, not a first step. Get the correct O(n) or O(mn) solution first. Then observe which previous states each cell needs and compress to a rolling array. Interviewers appreciate this structured approach.