Dynamic Programming

1D DP | 2D DP | Knapsack | LCS | Intervals | State Machines

šŸ“˜ 12 Sections 🧩 2 Solved Problems Advanced Difficulty Prerequisite: Ch9

1 — What Is Dynamic Programming?

Dynamic Programming (DP) solves problems by breaking them into overlapping subproblems and storing the results of each subproblem to avoid redundant computation. It is applicable when a problem has optimal substructure (an optimal solution is built from optimal sub-solutions) and overlapping subproblems (the same subproblem is solved multiple times in naive recursion).

Two Implementation Styles

  • TOP-DOWN (Memoisation): Write a recursive solution, add a memo table to cache results. Natural translation from the recurrence relation. Call the function with the original problem size.
  • BOTTOM-UP (Tabulation): Fill a DP table iteratively, starting from the smallest subproblems. Usually more space-efficient and avoids recursion stack overhead. Preferred in production code.
  • Both styles have the same asymptotic complexity. Top-down is often easier to derive; bottom-up is often easier to space-optimise.
  • Space optimisation: many DP problems only need the previous row/value, allowing O(n²) space to reduce to O(n) or O(1).

1.1 — The Four-Step DP Framework

  1. STEP 1 — DEFINE THE STATE: What does dp[i] (or dp[i][j]) represent? Be precise. Write it in plain English first.
  2. STEP 2 — WRITE THE RECURRENCE: How does dp[i] relate to smaller subproblems dp[i-1], dp[i-2], ...? This is the heart of the solution.
  3. STEP 3 — IDENTIFY BASE CASES: What are the smallest subproblems with known answers? Fill these first to avoid out-of-bounds access.
  4. STEP 4 — DETERMINE FILL ORDER: Which order guarantees that when computing dp[i], all required dp[j < i] are already filled? (Usually left-to-right for 1D, top-left to bottom-right for 2D.)

1.2 — DP vs Greedy vs Backtracking

Dimension Backtracking Greedy Dynamic Programming
Core idea Explore all paths, prune invalid One locally optimal choice per step Store & reuse overlapping subproblem results
Correctness requirement No requirement — explores all Greedy choice property + optimal substructure Optimal substructure alone
Output All solutions (or one) One optimal value One optimal value (or count)
Complexity Exponential (pruned) O(n) or O(n log n) Polynomial: O(n²), O(n*W), etc.
When to use Constraint satisfaction, enumeration Interval scheduling, greedy exchange Sequence, knapsack, string, path problems
Space O(depth) O(1) or O(n) O(n) to O(n²)

2 — Visual Diagrams: DP in Action

Diagram 1 — Fibonacci: Naive vs Memoised

Fibonacci: Why Memoisation Eliminates Redundancy
  fib(5) — NAIVE RECURSION (no memoisation):
                    fib(5)
                  /       \
              fib(4)       fib(3)
             /     \       /    \
          fib(3) fib(2) fib(2) fib(1)
          /   \
       fib(2) fib(1)

  fib(2) computed 3 times, fib(3) computed 2 times -> exponential O(2^n).

  fib(5) — WITH MEMOISATION:
  memo = {}
  fib(5) -> fib(4) -> fib(3) -> fib(2) -> fib(1) = 1, fib(0) = 0
  fib(2) = 1 -> stored in memo[2]
  fib(3) = fib(2)+fib(1) = 2 -> stored in memo[3]
  fib(4) = fib(3)+fib(2) = 3 -> stored in memo[4]  <- fib(2) read from memo
  fib(3) already in memo -> return memo[3] = 2
  fib(5) = fib(4)+fib(3) = 5

  Each subproblem solved ONCE. Total: O(n) time, O(n) space.

Diagram 2 — Coin Change DP Table

Coin Change: Full DP Table Trace
  coins = [1, 5, 6, 9]   amount = 11
  dp[i] = minimum coins to make amount i.  dp[0] = 0, rest = INF initially.

  i=0:  dp[0] = 0
  i=1:  try coin 1: dp[1-1]+1 = dp[0]+1 = 1.  dp[1] = 1
  i=2:  try coin 1: dp[1]+1 = 2.  dp[2] = 2
  i=3:  try coin 1: dp[2]+1 = 3.  dp[3] = 3
  i=4:  try coin 1: dp[3]+1 = 4.  dp[4] = 4
  i=5:  try coin 1: dp[4]+1 = 5.
        try coin 5: dp[0]+1 = 1.  dp[5] = 1   <- coin 5 wins
  i=6:  try coin 1: dp[5]+1 = 2.
        try coin 5: dp[1]+1 = 2.
        try coin 6: dp[0]+1 = 1.  dp[6] = 1   <- coin 6 wins
  i=7:  try coin 1: dp[6]+1 = 2.
        try coin 5: dp[2]+1 = 3.
        try coin 6: dp[1]+1 = 2.  dp[7] = 2
  i=8:  coin 1: dp[7]+1=3. coin 5: dp[3]+1=4. coin 6: dp[2]+1=3. dp[8]=3
  i=9:  coin 1: dp[8]+1=4. coin 5: dp[4]+1=5. coin 6: dp[3]+1=4.
        coin 9: dp[0]+1=1.  dp[9] = 1   <- coin 9 wins
  i=10: coin 1: dp[9]+1=2. coin 5: dp[5]+1=2. coin 6: dp[4]+1=5.
        coin 9: dp[1]+1=2.  dp[10] = 2
  i=11: coin 1: dp[10]+1=3. coin 5: dp[6]+1=2. coin 6: dp[5]+1=2.
        coin 9: dp[2]+1=3.  dp[11] = 2

  Answer: dp[11] = 2  (coins 5+6 or coins 2+9).  Correct!

Diagram 3 — LCS DP Table

LCS: DP Table Fill and Traceback
  s1 = 'ABCDE'   s2 = 'ACE'
  dp[i][j] = LCS length of s1[0..i-1] and s2[0..j-1]

       ''  A  C  E
    ''  0  0  0  0
    A   0  1  1  1
    B   0  1  1  1
    C   0  1  2  2
    D   0  1  2  2
    E   0  1  2  3   <- answer: dp[5][3] = 3

  Recurrence:
  if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1  (extend LCS)
  else:                  dp[i][j] = max(dp[i-1][j], dp[i][j-1])

  Trace the LCS: start at dp[5][3]=3.
  E==E (match): go diagonal to dp[4][2]=2. Track 'E'.
  D!=C:  max(dp[3][2], dp[4][1]) = max(2,1) -> came from dp[3][2].
  C==C (match): go diagonal to dp[2][1]=1. Track 'C'.
  B!=A:  max(dp[1][1], dp[2][0]) = max(1,0) -> came from dp[1][1].
  A==A (match): go diagonal to dp[0][0]=0. Track 'A'.
  LCS = 'ACE' (reversed tracking).  Length 3.  Correct!

Diagram 4 — 0/1 Knapsack DP Table

0/1 Knapsack: DP Table Fill
  items = [(w=2,v=6), (w=3,v=10), (w=4,v=12)]   capacity W = 5
  dp[i][w] = max value using first i items with capacity w.

        w: 0   1   2   3   4   5
  i=0    : 0   0   0   0   0   0
  i=1(2,6) : 0   0   6   6   6   6
  i=2(3,10): 0   0   6  10  10  16  <- 16 = v1+v2 = 6+10
  i=3(4,12): 0   0   6  10  12  16

  Answer: dp[3][5] = 16.  Items selected: (w=2,v=6) + (w=3,v=10).

  Recurrence:
  if items[i-1].w > w: dp[i][w] = dp[i-1][w]                      // can't include
  else: dp[i][w] = max(dp[i-1][w],                                // exclude
                       dp[i-1][w - items[i-1].w] + items[i-1].v)  // include

3 — Real-World Use Cases

DP Problem Pattern Real-World Application System
Shortest / Longest Path Route optimisation, cheapest flight Google Maps, airline booking systems
Edit Distance (LCS/LIS) Diff algorithms, DNA alignment git diff, bioinformatics BLAST
Knapsack / Packing Budget allocation, resource scheduling Cloud cost optimisation, ad budget planning
Sequence alignment Spell correction, plagiarism detection Autocorrect, Turnitin, Grammarly
Interval DP Matrix chain multiplication, expression parsing Compilers, query optimisers
Bitmask DP Travelling Salesman, assignment problems Logistics routing, hardware scheduling
Stock trading DP Portfolio rebalancing with constraints Algorithmic trading systems
Text segmentation Word wrap, code formatter line breaking LaTeX typesetting, VS Code formatter
Probability DP Dice game probabilities, HMM decoding Speech recognition, Monte Carlo pricing

4 — Core Concepts: DP Patterns

4.1 — 1D DP: Climbing Stairs / Fibonacci

// Climbing Stairs (LC 70) — number of ways to reach step n
// dp[i] = ways to reach step i = dp[i-1] + dp[i-2]
// Time: O(n)  Space: O(1) after optimisation
int climbStairs(int n) {
    if (n <= 2) return n;
    int prev2 = 1, prev1 = 2;
    for (int i = 3; i <= n; i++) {
        int curr = prev1 + prev2;
        prev2 = prev1;
        prev1 = curr;
    }
    return prev1;
}
// Space optimised: only keep the last two values (rolling variables).
// General pattern: whenever dp[i] depends only on dp[i-1] and dp[i-2],
// you can reduce O(n) space to O(1).

4.2 — 1D DP: Coin Change

// LeetCode 322 — Coin Change
// Minimum number of coins to make amount. Unlimited supply per denomination.
// Time: O(amount * n)  Space: O(amount)
int coinChange(vector<int>& coins, int amount) {
    vector<int> dp(amount+1, amount+1); // INF = amount+1 (impossible sentinel)
    dp[0] = 0;                          // base case: 0 coins for amount 0
    for (int i = 1; i <= amount; i++) {
        for (int c : coins) {
            if (c <= i)
                dp[i] = min(dp[i], dp[i-c] + 1);
        }
    }
    return dp[amount] > amount ? -1 : dp[amount];
}
// Why amount+1 as INF? Any valid answer is <= amount (using all 1-coins).
// If dp[amount] > amount after filling, no solution exists.

4.3 — 1D DP: Longest Increasing Subsequence (LIS)

// LeetCode 300 — Longest Increasing Subsequence
// Time: O(n^2)  Space: O(n)   [O(n log n) with patience sorting]
int lengthOfLIS(vector<int>& nums) {
    int n = nums.size();
    vector<int> dp(n, 1);  // dp[i] = LIS ending at index i
    int best = 1;
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i])           // nums[i] extends LIS at j
                dp[i] = max(dp[i], dp[j]+1);
        }
        best = max(best, dp[i]);
    }
    return best;
}

// O(n log n) version using binary search + patience sorting
int lengthOfLIS_nlogn(vector<int>& nums) {
    vector<int> tails; // tails[i] = smallest tail of all IS of length i+1
    for (int x : nums) {
        auto it = lower_bound(tails.begin(), tails.end(), x);
        if (it == tails.end()) tails.push_back(x);  // extend
        else                  *it = x;              // replace
    }
    return (int)tails.size();
}

4.4 — 2D DP: Longest Common Subsequence

// LeetCode 1143 — Longest Common Subsequence
// Time: O(m*n)  Space: O(m*n) -> O(n) with rolling array
int longestCommonSubsequence(string s1, string s2) {
    int m = s1.size(), n = s2.size();
    vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (s1[i-1] == s2[j-1])
                dp[i][j] = dp[i-1][j-1] + 1;      // chars match: extend
            else
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]); // skip one char
        }
    }
    return dp[m][n];
}

// Space-optimised: O(n) using two rolling rows
int lcs_space(string s1, string s2) {
    int m = s1.size(), n = s2.size();
    vector<int> prev(n+1,0), curr(n+1,0);
    for (int i=1;i<=m;i++) {
        for (int j=1;j<=n;j++)
            curr[j] = s1[i-1]==s2[j-1] ? prev[j-1]+1 : max(prev[j],curr[j-1]);
        swap(prev, curr);
        fill(curr.begin(), curr.end(), 0);
    }
    return prev[n];
}

4.5 — 0/1 Knapsack

// 0/1 Knapsack: each item used at most once
// Time: O(n*W)  Space: O(W) with 1D optimisation
int knapsack(vector<int>& w, vector<int>& v, int W) {
    int n = w.size();
    vector<int> dp(W+1, 0);
    for (int i = 0; i < n; i++) {
        // CRITICAL: iterate W down to w[i] to prevent using item i twice
        for (int cap = W; cap >= w[i]; cap--) {
            dp[cap] = max(dp[cap], dp[cap - w[i]] + v[i]);
        }
    }
    return dp[W];
}
// Why iterate capacity in REVERSE for 0/1 knapsack?
// When filling dp[cap], we need dp[cap-w[i]] from the PREVIOUS item iteration.
// Iterating forward would update dp[cap-w[i]] first, allowing item i to be
// used multiple times. Reverse iteration preserves the 'at most once' constraint.

// Unbounded Knapsack (items reusable): iterate capacity FORWARD
int unboundedKnapsack(vector<int>& w, vector<int>& v, int W) {
    vector<int> dp(W+1, 0);
    for (int cap = 1; cap <= W; cap++)
        for (int i = 0; i < (int)w.size(); i++)
            if (w[i] <= cap)
                dp[cap] = max(dp[cap], dp[cap - w[i]] + v[i]);
    return dp[W];
}

4.6 — State Machine DP: Stock Problems

// Best Time to Buy and Sell Stock with Cooldown (LC 309)
// States: held (holding stock), sold (just sold), rest (cooldown done)
// Time: O(n)  Space: O(1)
int maxProfit(vector<int>& prices) {
    int held = INT_MIN, sold = 0, rest = 0;
    for (int p : prices) {
        int prevHeld = held, prevSold = sold, prevRest = rest;
        held = max(prevHeld, prevRest - p);  // hold: keep or buy from rest
        sold = prevHeld + p;                  // sell: was holding, now sell
        rest = max(prevRest, prevSold);       // rest: keep resting or cooldown
    }
    return max(sold, rest); // can't end holding stock
}
// State transition diagram:
// rest -> held (buy)    held -> sold (sell)    sold -> rest (cooldown)
// rest -> rest (wait)   held -> held (hold)

4.7 — Interval DP: Burst Balloons

// LeetCode 312 — Burst Balloons
// dp[l][r] = max coins from bursting all balloons between l and r (exclusive)
// Key: think of k as the LAST balloon to burst in [l,r], not the first.
// Time: O(n^3)  Space: O(n^2)
int maxCoins(vector<int>& nums) {
    int n = nums.size();
    // Pad with sentinel 1s at both ends
    vector<int> a(n+2);
    a[0] = a[n+1] = 1;
    for (int i=0;i<n;i++) a[i+1] = nums[i];
    n += 2;
    vector<vector<int>> dp(n, vector<int>(n, 0));
    // len = length of open interval (l,r)
    for (int len = 2; len < n; len++) {
        for (int l = 0; l < n-len; l++) {
            int r = l + len;
            for (int k = l+1; k < r; k++) { // k is last burst in (l,r)
                dp[l][r] = max(dp[l][r],
                    dp[l][k] + a[l]*a[k]*a[r] + dp[k][r]);
            }
        }
    }
    return dp[0][n-1];
}

5 — Pattern Recognition Guide

DP Pattern Problem Signals State Definition Recurrence Shape
Linear 1D Single sequence, previous results matter dp[i] = answer for prefix i dp[i] = f(dp[i-1], dp[i-2], ...)
Two-sequence 2D Two strings/arrays, alignment/matching dp[i][j] = answer for s1[0..i], s2[0..j] dp[i][j] = f(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
Knapsack Items with weight+value, capacity constraint dp[i][w] = max value with i items, cap w include/exclude item i
Interval Optimal over all sub-intervals [i,j] dp[i][j] = answer for subarray [i,j] min/max over all splits k
State machine Finite states with transitions (stocks) dp[state] = best value in this state transitions between states per element
Counting paths Count ways (not optimise) dp[i] = number of ways to reach i sum of dp[j] for valid j
Digit DP Count numbers in range with digit constraints dp[pos][tight][...] = count try each digit, maintain tight constraint
Bitmask DP Small n (~20), subsets of items dp[mask] = answer for subset mask dp[mask | (1<<i)] = f(dp[mask])

How to Identify a DP Problem

  • SIGNAL 1: 'Count the number of ways' or 'Find the minimum/maximum'. These are classic DP outputs.
  • SIGNAL 2: 'Optimal decisions over a sequence' — each decision affects future options.
  • SIGNAL 3: Future choices depend on past choices (state carries forward).
  • SIGNAL 4: Greedy gives wrong answer (try a small counter-example). Backtracking TLEs. DP is the middle ground.
  • SIGNAL 5: The problem has well-defined subproblems that can be expressed as 'solve for i' or 'solve for (i,j)'.
  • NOT DP: problems with no ordering, problems that require enumerating all solutions, or problems solvable in O(n) with greedy.

Space Optimisation Decision Table

  • dp[i] depends on dp[i-1] only → use two variables (O(n) → O(1)).
  • dp[i] depends on dp[i-1] and dp[i-2] → use three rolling variables (O(n) → O(1)).
  • dp[i][j] depends on dp[i-1][...] only → use two 1D arrays (O(n²) → O(n)).
  • dp[i][j] depends on dp[i-1][j-1] → use one 1D array with careful update order.
  • 0/1 knapsack 2D → 1D by iterating capacity in REVERSE (prevents item reuse).
  • Unbounded knapsack 2D → 1D by iterating capacity FORWARD (allows item reuse).

6 — Complete C++ Implementations

6.1 — Edit Distance (Levenshtein)

// LeetCode 72 — Edit Distance
// Min operations (insert, delete, replace) to convert word1 to word2.
// Time: O(m*n)  Space: O(n) rolling array
int minDistance(string s, string t) {
    int m = s.size(), n = t.size();
    vector<int> dp(n+1);
    iota(dp.begin(), dp.end(), 0); // dp[j] = j (delete j chars from t)
    for (int i = 1; i <= m; i++) {
        int prev = dp[0]; // dp[i-1][j-1] before overwrite
        dp[0] = i;        // dp[i][0] = i (delete i chars from s)
        for (int j = 1; j <= n; j++) {
            int temp = dp[j];
            if (s[i-1] == t[j-1])
                dp[j] = prev;           // chars match: no operation
            else
                dp[j] = 1 + min({prev,  // replace
                                 dp[j],  // delete from s
                                 dp[j-1]}); // insert into s
            prev = temp;
        }
    }
    return dp[n];
}

6.2 — Unique Paths on Grid

// LeetCode 62 — Unique Paths
// Count paths from top-left to bottom-right moving only right or down.
// Time: O(m*n)  Space: O(n)
int uniquePaths(int m, int n) {
    vector<int> dp(n, 1); // first row: all 1s (only rightward path)
    for (int i = 1; i < m; i++)
        for (int j = 1; j < n; j++)
            dp[j] += dp[j-1]; // paths from top + paths from left
    return dp[n-1];
}
// Mathematical answer: C(m+n-2, m-1) — choose m-1 down moves from m+n-2 total.

// LC 63 — Unique Paths II (with obstacles)
int uniquePathsWithObstacles(vector<vector<int>>& grid) {
    int m = grid.size(), n = grid[0].size();
    vector<long long> dp(n, 0);
    dp[0] = grid[0][0] == 0 ? 1 : 0;
    for (int i = 0; i < m; i++) {
        if (grid[i][0] == 1) dp[0] = 0; // obstacle blocks leftmost column
        for (int j = 1; j < n; j++)
            dp[j] = grid[i][j] == 1 ? 0 : dp[j] + dp[j-1];
    }
    return (int)dp[n-1];
}

6.3 — Word Break

// LeetCode 139 — Word Break
// Can string s be segmented into words from wordDict?
// Time: O(n^2 * L)  L=avg word length  Space: O(n)
bool wordBreak(string s, vector<string>& wordDict) {
    unordered_set<string> ws(wordDict.begin(), wordDict.end());
    int n = s.size();
    vector<bool> dp(n+1, false);
    dp[0] = true; // empty string always segmentable
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j < i; j++) {
            // If prefix s[0..j-1] is breakable AND s[j..i-1] is a word
            if (dp[j] && ws.count(s.substr(j, i-j))) {
                dp[i] = true;
                break; // found one valid split, no need to check more
            }
        }
    }
    return dp[n];
}

7 — Complexity Reference

Algorithm Time Space
Fibonacci / Climbing Stairs O(n) O(1) rolling
Coin Change (min coins) O(amount * n) O(amount)
Coin Change II (count ways) O(amount * n) O(amount)
Longest Increasing Subsequence (O(n²)) O(n²) O(n)
LIS O(n log n) with patience sort O(n log n) O(n)
Longest Common Subsequence O(m*n) O(n) rolling
Edit Distance O(m*n) O(n) rolling
0/1 Knapsack O(n*W) O(W) 1D
Unbounded Knapsack / Coin Change O(amount*n) O(amount)
Unique Paths on Grid O(m*n) O(n)
Word Break O(n² * L) O(n)
Stock with Cooldown / k transactions O(n * k) O(k)
Burst Balloons / Interval DP O(n³) O(n²)
Bitmask DP (TSP-style) O(2^n * n²) O(2^n * n)

Pseudo-polynomial vs Polynomial

  • Coin Change and Knapsack run in O(amount * n) or O(n * W). This looks polynomial but is actually pseudo-polynomial.
  • Reason: the input size is O(log amount) bits, not O(amount). A truly polynomial algorithm would be O(n * log(amount)).
  • In practice: if amount <= 10⁓ or W <= 10⁓, DP is fast enough. If amount ~ 10⁹, DP is infeasible.
  • Truly polynomial DP: LCS, LIS, Edit Distance are polynomial in input size n (length of the strings/arrays).

8 — Solved Problem 1: Coin Change

Given an integer array coins and an integer amount, return the fewest number of coins needed to make up amount. If it is impossible, return -1. You may use each coin denomination an unlimited number of times.

OBSERVATIONS

  • Greedy fails here. Example: coins=[1,5,6,9], amount=11. Greedy picks 9+1+1=3 coins. Optimal is 5+6=2 coins.
  • Optimal substructure: dp[i] = min coins for amount i = 1 + min(dp[i-c]) for each coin c where c <= i.
  • Overlapping subproblems: dp[6] is needed when computing dp[7], dp[8], dp[9], dp[11], ... Memoising dp[6] avoids recomputing it.
  • Unlimited coin use = unbounded knapsack variant. Iterate amounts from 1 to target, try each coin.
  • Initialise dp[0]=0 (base case: 0 coins for amount 0). dp[i] = amount+1 as sentinel for 'unreachable'.
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        // dp[i] = minimum coins needed to make amount i
        vector<int> dp(amount+1, amount+1); // sentinel: larger than any valid answer
        dp[0] = 0;  // base case

        for (int i = 1; i <= amount; i++) {
            for (int c : coins) {
                if (c <= i)  // coin c can contribute to amount i
                    dp[i] = min(dp[i], dp[i-c] + 1);
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
};

Complexity: Time O(amount * n), Space O(amount)

9 — Solved Problem 2: Longest Common Subsequence

Given two strings text1 and text2, return the length of their longest common subsequence (LCS). A subsequence is a sequence derived by deleting some characters without changing the relative order of the remaining characters.

OBSERVATIONS

  • A subsequence is NOT a substring — characters don't need to be contiguous.
  • State definition: dp[i][j] = length of LCS of text1[0..i-1] and text2[0..j-1]. 1-indexed to simplify base cases.
  • Recurrence: If text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 (extend LCS by matching char). Else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) (skip one char from either string).
  • Base cases: dp[0][j] = 0 (empty text1) and dp[i][0] = 0 (empty text2). Automatically initialised by vector constructor.
  • Fill order: row by row, left to right. dp[i][j] only depends on dp[i-1][j-1], dp[i-1][j], dp[i][j-1] — all already computed.
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        // Space-optimised: use two rows only
        vector<int> prev(n+1, 0), curr(n+1, 0);

        for (int i = 1; i <= m; i++) {
            fill(curr.begin(), curr.end(), 0);
            for (int j = 1; j <= n; j++) {
                if (text1[i-1] == text2[j-1])
                    curr[j] = prev[j-1] + 1;         // chars match
                else
                    curr[j] = max(prev[j], curr[j-1]); // skip one
            }
            swap(prev, curr);
        }
        return prev[n];
    }
};

Complexity: Time O(m*n), Space O(n)

10 — Common Mistakes & Edge Cases

10.1 — State Definition Mistakes

  • Imprecise state definition. dp[i] must have an exact meaning. 'dp[i] = something about index i' is not enough — write 'dp[i] = minimum cost to reach index i from index 0 using exactly i steps'.
  • Off-by-one in string/array DP. When using 1-indexed dp (dp[1..n] for string of length n), always access text[i-1] for the i-th character. Mixing 0-indexed and 1-indexed causes subtle bugs.
  • Wrong base case. For coin change, initialising dp[0] = 0 is critical. For LCS, the entire dp[0][j] and dp[i][0] row/column must be 0. For path counting, dp[0][0] = 1.
  • Missing the 'impossible' sentinel. For coin change, initialise to amount+1 (not INT_MAX — adding 1 to INT_MAX overflows). For paths, -1 or 0 are natural sentinels.

10.2 — Knapsack Direction Mistakes

  • 0/1 knapsack — iterating capacity forward in the 1D optimisation. This allows the same item to be used multiple times (becomes unbounded knapsack). Always iterate capacity in REVERSE for 0/1 knapsack.
  • Coin Change II (combinations) — iterating loops in wrong order. Iterating coins in inner loop and amounts in outer counts permutations (e.g. [1,2] and [2,1] counted separately). Swap the loops: outer = coins, inner = amounts.
  • Confusing 'at most W' capacity with 'exactly W'. For knapsack, dp[W] gives the answer for capacity AT MOST W. For 'exactly W' problems, initialise with -infinity except dp[0]=0.

10.3 — Edge Cases

  • Empty string LCS: if either string is empty, LCS = 0. Base cases dp[0][j]=dp[i][0]=0 handle this.
  • No valid coin combination: return -1, not 0. Check if dp[amount] > amount after filling.
  • amount = 0 for coin change: always return 0 (zero coins needed). dp[0] = 0 handles this.
  • Identical strings for LCS: LCS = the string itself. dp[n][n] = n.
  • All-negative array for max subarray: Kadane returns the single maximum element (not 0). Initialise both currSum and maxSum to nums[0].

11 — Common Interview Questions

Problem Key Implementation Detail
Climbing Stairs (LC 70) Fibonacci pattern, O(1) space
House Robber (LC 198) dp[i] = max(dp[i-1], dp[i-2]+nums[i])
Coin Change (LC 322) Unbounded knapsack, dp[i] = min(dp[i-c]+1)
Coin Change II (LC 518) count combinations, outer=coins inner=amounts
Longest Increasing Subsequence (LC 300) O(n²) DP or O(n log n) patience sort
Word Break (LC 139) dp[i] = any dp[j] && s[j..i] in dict
Longest Common Subsequence (LC 1143) Classic 2D DP, O(n) space opt
Edit Distance (LC 72) 3-way recurrence: insert/delete/replace
Unique Paths (LC 62) Grid path counting, O(n) space
Partition Equal Subset Sum (LC 416) 0/1 knapsack with target = sum/2
Best Time to Buy Stock with Cooldown (LC 309) State machine DP
Burst Balloons (LC 312) Interval DP, think last burst not first

Chapter 10 — Key Takeaways

  • DP requires BOTH optimal substructure (optimal solution built from optimal sub-solutions) AND overlapping subproblems.
  • Four steps: define state precisely, write recurrence, identify base cases, determine fill order.
  • Top-down vs Bottom-up: Top-down = recursion + memo cache. Bottom-up = iterative table. Same complexity; bottom-up avoids stack overhead.
  • 1D DP space opt: rolling variables when dp[i] depends on dp[i-1] and dp[i-2] only.
  • 2D DP space opt: two 1D arrays (prev/curr) when dp[i][j] depends only on row i-1.
  • 0/1 knapsack 1D: iterate capacity in REVERSE to prevent item reuse.
  • Unbounded knapsack / Coin Change: iterate capacity FORWARD to allow item reuse.
  • Coin Change: outer=amounts, inner=coins. Coin Change II: outer=coins, inner=amounts.
  • LCS recurrence: match → diagonal+1. Mismatch → max(up, left).