On This Page

1. What is Dynamic Programming? 2. Top-Down vs Bottom-Up 3. The DP Problem-Solving Framework 4. Fibonacci (The Classic Intro) 5. Climbing Stairs 6. 1D DP Problems 7. 2D DP Problems 8. DP Categories Cheat Sheet 9. LeetCode Problems by Category 10. Tips for DP 11. Practice Quiz

1. What is Dynamic Programming?

Dynamic Programming (DP) is a technique for solving problems by breaking them into overlapping subproblems and storing the results so you never solve the same subproblem twice. That is literally all it is.

For a problem to be solvable with DP, it needs two key properties:

  1. Optimal substructure -- the optimal solution to the problem contains optimal solutions to its subproblems. If you can build the answer from answers to smaller versions of the same problem, you have this.
  2. Overlapping subproblems -- the same subproblems are solved multiple times. This is why storing results helps. If each subproblem is unique (like in merge sort), DP won't help -- that is just divide and conquer.
DP = Recursion + Memoization (or Bottom-Up Tabulation)
"Is This a DP Problem?" -- Checklist:

1. Can the problem be broken into overlapping subproblems? (same subproblem solved multiple times)
2. Does the problem have optimal substructure? (optimal solution uses optimal solutions to subproblems)
3. Is the problem asking for: count, min/max, or feasibility?

If YES to all three → try DP.

Complexity rule: Time = (number of distinct states) × (work per state)
Space = number of states stored simultaneously
Don't Panic

DP is NOT as scary as people make it sound. If you can write a recursive solution, you can convert it to DP. The hard part is defining what your subproblem is -- the actual coding is usually straightforward. Think of DP as "remembering your work so you don't redo it."

Why naive recursion is slow

Consider computing Fibonacci(5). A naive recursive approach recalculates the same values over and over:

/*
                     fib(5)
                   /        \
              fib(4)         fib(3)
             /    \          /    \
         fib(3)  fib(2)  fib(2)  fib(1)
         /   \    / \     / \
     fib(2) fib(1) ...  ...
      / \
 fib(1) fib(0)

 fib(3) is computed 2 times
 fib(2) is computed 3 times
 This EXPLODES exponentially as n grows
*/

With DP, you compute each value once, store it, and look it up next time. That takes you from O(2^n) to O(n).

2. Top-Down (Memoization) vs Bottom-Up (Tabulation)

There are two ways to implement DP. Both give the same results -- they just approach the problem from different directions.

Top-Down (Memoization)

Bottom-Up (Tabulation)

Aspect Top-Down (Memo) Bottom-Up (Tab)
DirectionBig problem down to base casesBase cases up to big problem
ImplementationRecursion + cacheIterative + table
Ease of thoughtOften easier to deriveRequires knowing computation order
Stack overflow riskYes, for large inputsNo
Space optimizationHarderEasier (can drop old rows/values)
Computes unused states?No (only what's needed)Sometimes yes
Recommendation

In interviews, start with top-down -- it is the natural extension of your recursive brute force. Once that works, mention you could convert to bottom-up for better space/performance. We will show both approaches for every problem below.

3. The DP Problem-Solving Framework

Use this 5-step framework for every DP problem. Seriously -- every single one.

1
Define the state What does dp[i] (or dp[i][j]) represent?
2
Find the recurrence dp[i] = f(dp[i-1], dp[i-2], ...)
3
Set base cases dp[0] = ?, dp[1] = ?
4
Determine computation order Left to right? Bottom to top?
5
Optimize space Do you really need the whole table?
Example: Applying the framework

Problem: "How many ways to reach step n if you can take 1 or 2 steps?"

  1. State: dp[i] = number of ways to reach step i
  2. Recurrence: dp[i] = dp[i-1] + dp[i-2] (arrive from one step back OR two steps back)
  3. Base cases: dp[0] = 1 (one way to be at the start), dp[1] = 1 (one way to reach step 1)
  4. Order: Left to right (i = 2, 3, ..., n)
  5. Space: Only need last two values, so O(1) instead of O(n)

4. Fibonacci (The Classic Intro)

Every DP tutorial starts with Fibonacci for a reason: it perfectly illustrates why DP exists. The Fibonacci sequence is 0, 1, 1, 2, 3, 5, 8, 13, 21, ... where each number is the sum of the two before it.

fib(n) = fib(n-1) + fib(n-2), where fib(0) = 0, fib(1) = 1

Approach 1: Naive Recursion -- O(2^n) time, O(n) space

This is what NOT to do. Each call branches into two more, creating exponential work.

Python
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

# fib(40) takes SECONDS. fib(50) may never finish.
JavaScript
function fib(n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}

Approach 2: Top-Down with Memoization -- O(n) time, O(n) space

Same recursion, but we remember every result. Each fib(k) is computed exactly once.

Python
def fib(n, memo={}):
    if n <= 1:
        return n
    if n in memo:
        return memo[n]
    memo[n] = fib(n - 1, memo) + fib(n - 2, memo)
    return memo[n]

# fib(1000) returns instantly
JavaScript
function fib(n, memo = {}) {
    if (n <= 1) return n;
    if (memo[n] !== undefined) return memo[n];
    memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
    return memo[n];
}

Approach 3: Bottom-Up Tabulation -- O(n) time, O(n) space

Build the table iteratively from the base cases up. No recursion at all.

Python
def fib(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]
JavaScript
function fib(n) {
    if (n <= 1) return n;
    const dp = new Array(n + 1).fill(0);
    dp[1] = 1;
    for (let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}
State Table Walkthrough

Computing fib(6) bottom-up:

i0123456
dp[i]0112358

Each cell is just the sum of the two cells to its left. That is it.

Approach 4: Space-Optimized -- O(n) time, O(1) space

We only ever look at the last two values, so why store the whole array?

Python
def fib(n):
    if n <= 1:
        return n
    prev2, prev1 = 0, 1
    for _ in range(2, n + 1):
        curr = prev1 + prev2
        prev2 = prev1
        prev1 = curr
    return prev1
JavaScript
function fib(n) {
    if (n <= 1) return n;
    let prev2 = 0, prev1 = 1;
    for (let i = 2; i <= n; i++) {
        const curr = prev1 + prev2;
        prev2 = prev1;
        prev1 = curr;
    }
    return prev1;
}
ApproachTimeSpace
Naive recursionO(2^n)O(n)
Top-down memoO(n)O(n)
Bottom-up tableO(n)O(n)
Space-optimizedO(n)O(1)

5. Climbing Stairs

Problem: You are climbing a staircase with n steps. Each time you can climb 1 or 2 steps. How many distinct ways can you reach the top?

State: dp[i] = number of ways to reach step i
Recurrence: dp[i] = dp[i-1] + dp[i-2]
Base cases: dp[0] = 1, dp[1] = 1

Wait -- this is just Fibonacci! To reach step i, you either came from step i-1 (one step) or step i-2 (two steps). The total ways is the sum of both.

State Table: n = 5
Step012345
Ways112358

There are 8 distinct ways to climb 5 stairs.

Top-Down

Python
def climbStairs(n, memo={}):
    if n <= 1:
        return 1
    if n in memo:
        return memo[n]
    memo[n] = climbStairs(n - 1, memo) + climbStairs(n - 2, memo)
    return memo[n]
JavaScript
function climbStairs(n, memo = {}) {
    if (n <= 1) return 1;
    if (memo[n] !== undefined) return memo[n];
    memo[n] = climbStairs(n - 1, memo) + climbStairs(n - 2, memo);
    return memo[n];
}

Bottom-Up (Space-Optimized)

Python
def climbStairs(n):
    if n <= 1:
        return 1
    prev2, prev1 = 1, 1
    for _ in range(2, n + 1):
        curr = prev1 + prev2
        prev2 = prev1
        prev1 = curr
    return prev1
JavaScript
function climbStairs(n) {
    if (n <= 1) return 1;
    let prev2 = 1, prev1 = 1;
    for (let i = 2; i <= n; i++) {
        const curr = prev1 + prev2;
        prev2 = prev1;
        prev1 = curr;
    }
    return prev1;
}

Time: O(n) | Space: O(1)

6. 1D DP Problems

House Robber

Problem: You are a robber planning to rob houses along a street. Each house has a certain amount of money. You cannot rob two adjacent houses (alarm triggers). Find the maximum amount you can rob.

State: dp[i] = max money robbing houses 0..i
Recurrence: dp[i] = max(dp[i-1], dp[i-2] + nums[i])
Base cases: dp[0] = nums[0], dp[1] = max(nums[0], nums[1])

At each house, you have two choices: skip it (take dp[i-1]) or rob it (take dp[i-2] + this house's money). Pick whichever is more.

State Table: nums = [2, 7, 9, 3, 1]
i01234
nums[i]27931
dp[i]27111112

dp[2] = max(7, 2+9) = 11. dp[3] = max(11, 7+3) = 11. dp[4] = max(11, 11+1) = 12. Rob houses 0, 2, 4 for $12.

Top-Down

Python
def rob(nums):
    memo = {}
    def dp(i):
        if i < 0:
            return 0
        if i in memo:
            return memo[i]
        memo[i] = max(dp(i - 1), dp(i - 2) + nums[i])
        return memo[i]
    return dp(len(nums) - 1)
JavaScript
function rob(nums) {
    const memo = {};
    function dp(i) {
        if (i < 0) return 0;
        if (memo[i] !== undefined) return memo[i];
        memo[i] = Math.max(dp(i - 1), dp(i - 2) + nums[i]);
        return memo[i];
    }
    return dp(nums.length - 1);
}

Bottom-Up (Space-Optimized)

Python
def rob(nums):
    if not nums:
        return 0
    if len(nums) == 1:
        return nums[0]
    prev2, prev1 = 0, 0
    for num in nums:
        curr = max(prev1, prev2 + num)
        prev2 = prev1
        prev1 = curr
    return prev1
JavaScript
function rob(nums) {
    let prev2 = 0, prev1 = 0;
    for (const num of nums) {
        const curr = Math.max(prev1, prev2 + num);
        prev2 = prev1;
        prev1 = curr;
    }
    return prev1;
}

Time: O(n) | Space: O(1)

Maximum Subarray (Kadane's Algorithm)

Problem: Find the contiguous subarray with the largest sum.

State: dp[i] = maximum subarray sum ending at index i
Recurrence: dp[i] = max(nums[i], dp[i-1] + nums[i])
Base case: dp[0] = nums[0]

At each element, you decide: start a new subarray here, or extend the previous one. If the running sum is negative, start fresh.

State Table: nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
i012345678
nums[i]-21-34-121-54
dp[i]-21-2435615

Maximum is 6 (subarray [4, -1, 2, 1]).

Python
def maxSubArray(nums):
    max_sum = curr_sum = nums[0]
    for num in nums[1:]:
        curr_sum = max(num, curr_sum + num)
        max_sum = max(max_sum, curr_sum)
    return max_sum
JavaScript
function maxSubArray(nums) {
    let maxSum = nums[0], currSum = nums[0];
    for (let i = 1; i < nums.length; i++) {
        currSum = Math.max(nums[i], currSum + nums[i]);
        maxSum = Math.max(maxSum, currSum);
    }
    return maxSum;
}

Time: O(n) | Space: O(1)

Coin Change

Problem: Given coin denominations and a target amount, find the minimum number of coins needed. If impossible, return -1.

State: dp[i] = minimum coins needed for amount i
Recurrence: dp[i] = min(dp[i - coin] + 1) for each coin where coin <= i
Base case: dp[0] = 0
State Table: coins = [1, 3, 4], amount = 6
Amount0123456
dp[i]0121122

dp[6] = 2 (use coins 3 + 3). Note: greedy (4+1+1 = 3 coins) fails here!

Top-Down

Python
def coinChange(coins, amount):
    memo = {}
    def dp(remaining):
        if remaining == 0:
            return 0
        if remaining < 0:
            return float('inf')
        if remaining in memo:
            return memo[remaining]
        memo[remaining] = min(dp(remaining - c) + 1 for c in coins)
        return memo[remaining]

    result = dp(amount)
    return result if result != float('inf') else -1
JavaScript
function coinChange(coins, amount) {
    const memo = {};
    function dp(remaining) {
        if (remaining === 0) return 0;
        if (remaining < 0) return Infinity;
        if (memo[remaining] !== undefined) return memo[remaining];
        let min = Infinity;
        for (const coin of coins) {
            min = Math.min(min, dp(remaining - coin) + 1);
        }
        memo[remaining] = min;
        return min;
    }
    const result = dp(amount);
    return result === Infinity ? -1 : result;
}

Bottom-Up

Python
def coinChange(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for i in range(1, amount + 1):
        for coin in coins:
            if coin <= i:
                dp[i] = min(dp[i], dp[i - coin] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1
JavaScript
function coinChange(coins, amount) {
    const dp = new Array(amount + 1).fill(Infinity);
    dp[0] = 0;
    for (let i = 1; i <= amount; i++) {
        for (const coin of coins) {
            if (coin <= i) {
                dp[i] = Math.min(dp[i], dp[i - coin] + 1);
            }
        }
    }
    return dp[amount] === Infinity ? -1 : dp[amount];
}

Time: O(amount * coins) | Space: O(amount)

Why Greedy Fails

For coins [1, 3, 4] and amount 6, greedy picks 4+1+1 = 3 coins. DP correctly finds 3+3 = 2 coins. This is exactly why DP exists -- it explores all possibilities without brute-forcing every combination.

Longest Increasing Subsequence (LIS)

Problem: Find the length of the longest strictly increasing subsequence (not necessarily contiguous).

State: dp[i] = length of LIS ending at index i
Recurrence: dp[i] = max(dp[j] + 1) for all j < i where nums[j] < nums[i]
Base case: dp[i] = 1 for all i (each element is a subsequence of length 1)
State Table: nums = [10, 9, 2, 5, 3, 7, 101, 18]
i01234567
nums[i]109253710118
dp[i]11122344

LIS length = 4. One possible LIS: [2, 3, 7, 101] or [2, 5, 7, 101] or [2, 5, 7, 18].

Python
def lengthOfLIS(nums):
    n = len(nums)
    dp = [1] * n
    for i in range(1, n):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)
JavaScript
function lengthOfLIS(nums) {
    const n = nums.length;
    const dp = new Array(n).fill(1);
    for (let i = 1; i < n; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    return Math.max(...dp);
}

Time: O(n^2) | Space: O(n)

For interviews

There is an O(n log n) solution using binary search + patience sorting. Know it exists and can explain the idea, but the O(n^2) DP solution is perfectly acceptable in most interviews. The O(n log n) version is rarely expected unless specifically asked.

7. 2D DP Problems

When your state depends on two variables (two indices, a position in a grid, etc.), you need a 2D DP table.

Unique Paths

Problem: Given an m x n grid, starting at top-left, you can only move right or down. How many unique paths to the bottom-right?

State: dp[i][j] = number of ways to reach cell (i, j)
Recurrence: dp[i][j] = dp[i-1][j] + dp[i][j-1]
Base cases: dp[0][j] = 1 (first row), dp[i][0] = 1 (first column)
State Table: 3x4 grid
col 0col 1col 2col 3
row 01111
row 11234
row 213610

Answer: 10 unique paths. Each cell = sum of cell above + cell to the left.

Python
def uniquePaths(m, n):
    dp = [[1] * n for _ in range(m)]
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
    return dp[m - 1][n - 1]
JavaScript
function uniquePaths(m, n) {
    const dp = Array.from({ length: m }, () =>
        new Array(n).fill(1)
    );
    for (let i = 1; i < m; i++) {
        for (let j = 1; j < n; j++) {
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
        }
    }
    return dp[m - 1][n - 1];
}

Time: O(m * n) | Space: O(m * n), can be optimized to O(n) using a single row

Longest Common Subsequence (LCS)

Problem: Given two strings, find the length of their longest common subsequence.

State: dp[i][j] = LCS length of text1[0..i-1] and text2[0..j-1]
Recurrence:
  If text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
  Else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
Base cases: dp[0][j] = 0, dp[i][0] = 0
State Table: text1 = "abcde", text2 = "ace"
""ace
""0000
a0111
b0111
c0122
d0122
e0123

LCS length = 3. The LCS is "ace".

Python
def longestCommonSubsequence(text1, text2):
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i - 1] == text2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[m][n]
JavaScript
function longestCommonSubsequence(text1, text2) {
    const m = text1.length, n = text2.length;
    const dp = Array.from({ length: m + 1 }, () =>
        new Array(n + 1).fill(0)
    );
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (text1[i - 1] === text2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[m][n];
}

Time: O(m * n) | Space: O(m * n)

0/1 Knapsack

Problem: Given items with weights and values, and a knapsack with weight capacity W, maximize the total value you can carry. Each item can only be used once.

State: dp[i][w] = max value using items 0..i-1 with capacity w
Recurrence:
  If weights[i-1] <= w: dp[i][w] = max(dp[i-1][w], dp[i-1][w - weights[i-1]] + values[i-1])
  Else: dp[i][w] = dp[i-1][w]
Base cases: dp[0][w] = 0 for all w
State Table: weights=[1,3,4,5], values=[1,4,5,7], capacity=7
item\cap01234567
0 items00000000
item 1 (1,1)01111111
item 2 (3,4)01145555
item 3 (4,5)01145669
item 4 (5,7)01145789

Max value = 9 (items 2 and 3: weight 3+4=7, value 4+5=9).

Python
def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for w in range(capacity + 1):
            if weights[i - 1] <= w:
                dp[i][w] = max(
                    dp[i - 1][w],
                    dp[i - 1][w - weights[i - 1]] + values[i - 1]
                )
            else:
                dp[i][w] = dp[i - 1][w]
    return dp[n][capacity]
JavaScript
function knapsack(weights, values, capacity) {
    const n = weights.length;
    const dp = Array.from({ length: n + 1 }, () =>
        new Array(capacity + 1).fill(0)
    );
    for (let i = 1; i <= n; i++) {
        for (let w = 0; w <= capacity; w++) {
            if (weights[i - 1] <= w) {
                dp[i][w] = Math.max(
                    dp[i - 1][w],
                    dp[i - 1][w - weights[i - 1]] + values[i - 1]
                );
            } else {
                dp[i][w] = dp[i - 1][w];
            }
        }
    }
    return dp[n][capacity];
}

Time: O(n * W) | Space: O(n * W), can be optimized to O(W) using a 1D array

8. DP Categories Cheat Sheet

Most DP problems fall into one of these categories. Recognizing the category is half the battle.

Category Example Problems Key Idea
Linear Fibonacci, Climbing Stairs, House Robber dp[i] depends on previous elements
Interval Palindromic Substrings, Matrix Chain Mult. dp[i][j] for range i to j
Grid Unique Paths, Min Path Sum dp[i][j] for grid position
String LCS, Edit Distance dp[i][j] for indices in two strings
Knapsack 0/1 Knapsack, Coin Change, Subset Sum Include or exclude item
Tree House Robber III, Tree Diameter DFS + memoization on tree nodes
Pattern Recognition

When you see a new DP problem, ask: "Which category is this?" If it involves a grid, try grid DP. If two strings, try string DP. If choosing to include/exclude items, try knapsack. This narrows down your approach immediately.

9. LeetCode Problems by Category

1D DP

ProblemDifficultyKey Pattern
70. Climbing StairsEasyFibonacci variant
198. House RobberMediumInclude/exclude adjacent
322. Coin ChangeMediumUnbounded knapsack
300. Longest Increasing SubsequenceMediumLIS pattern
139. Word BreakMediumString segmentation
91. Decode WaysMediumFibonacci-like with conditions

2D DP

ProblemDifficultyKey Pattern
62. Unique PathsMediumGrid traversal
1143. Longest Common SubsequenceMediumTwo-string comparison
72. Edit DistanceMediumTwo-string transformation
494. Target SumMediumKnapsack variant

Classic Patterns

ProblemDifficultyKey Pattern
0/1 KnapsackMediumInclude/exclude with capacity
Unbounded Knapsack (Coin Change)MediumReusable items with capacity
Study Order

Do them in this order: Climbing Stairs, House Robber, Coin Change, Unique Paths, LCS, then LIS. Each one builds on the concepts from the previous ones. After these six, the rest are variations.

10. Tips for DP

Common Mistakes
  • Forgetting base cases -- always handle dp[0], dp[1], empty arrays, empty strings
  • Off-by-one errors in loop bounds -- double-check inclusive vs exclusive ranges
  • Wrong computation order -- make sure dp[i] values you need are already computed
  • Not considering all transitions -- missing a case in your recurrence
  • Overcomplicating the state -- start simple, add dimensions only if needed

11. Practice Quiz

Q1: What are the two key properties required for a problem to be solvable with DP?

B) Optimal substructure + overlapping subproblems. Optimal substructure means the optimal solution contains optimal solutions to subproblems. Overlapping subproblems means the same subproblems are solved multiple times, which is why caching helps. Memoization and tabulation are implementation techniques, not properties of the problem itself.

Q2: What is the time complexity of naive recursive Fibonacci?

C) O(2^n). Each call branches into two recursive calls, creating an exponential number of function calls. The recursion tree has roughly 2^n nodes. Adding memoization reduces this to O(n) because each subproblem is computed only once.

Q3: For the House Robber problem with nums = [2, 7, 9, 3, 1], what is dp[2]?

B) 11. dp[2] = max(dp[1], dp[0] + nums[2]) = max(7, 2 + 9) = max(7, 11) = 11. We either skip house 2 (keeping 7 from house 1) or rob house 2 (adding 9 to the 2 we got from house 0).

Q4: In the Coin Change problem, why does a greedy approach fail for coins = [1, 3, 4] and amount = 6?

A) Greedy picks 4+1+1 (3 coins) instead of 3+3 (2 coins). The greedy approach always picks the largest coin possible, giving 4+1+1 = 3 coins. But the optimal solution is 3+3 = 2 coins. DP considers all possible combinations and finds the true minimum.

Q5: In a Unique Paths problem on a 3x3 grid, what is dp[2][2] (bottom-right)?

B) 6. The grid fills as: row 0 = [1,1,1], row 1 = [1,2,3], row 2 = [1,3,6]. Each cell is the sum of the cell above and the cell to the left. dp[2][2] = dp[1][2] + dp[2][1] = 3 + 3 = 6.