DSA DAY ONE
DSA DAY ONE
PART I — FOUNDATIONS
Chapter 1: C++ Essentials for DSA
Chapter 2: C++ STL Deep Dive
Chapter 3: Complexity Analysis
PART II — LINEAR DATA STRUCTURES
Chapter 4: Arrays & Strings
Chapter 5: Two Pointers & Sliding Window
Chapter 6: Hashing
Chapter 7: Linked Lists
Chapter 8: Stacks
Chapter 9: Queues & Deques
PART III — TREES
Chapter 10: Binary Trees
Chapter 11: Binary Search Trees (BST)
Chapter 12: Heaps & Priority Queues
Chapter 13: Tries (Prefix Trees)
Chapter 14: Segment Tree & Fenwick Tree
PART IV — ALGORITHMS
Chapter 15: Sorting Algorithms
Chapter 16: Binary Search
Chapter 17: Recursion & Backtracking
Chapter 18: Graphs — Fundamentals & Traversals
Chapter 19: Graph Algorithms — Shortest Paths, MST & Union-Find
Chapter 20: Dynamic Programming
DP Philosophy — What, Why & WhenMemoization — Top-Down DPTabulation — Bottom-Up DPState Definition — The Most Critical Skill1D DP — Linear State Problems2D DP — Grid & Two-Sequence ProblemsKnapsack FamilyString DP — LCS, Edit Distance & PalindromesLIS — Longest Increasing SubsequenceInterval DPTree DPBitmask DPState Machine DP — Stock ProblemsCommon DP Anti-PatternsSample ProblemsLeetCode Problem Set
Chapter 21: Greedy Algorithms
Chapter 22: Divide & Conquer
PART V — COMPETITIVE & MATH
Chapter 23: Bit Manipulation
Chapter 24: Math & Number Theory
PART VI — PATTERNS
Chapter 25: Pattern Recognition Mastery
PART VII — SYSTEM DESIGN
Chapter 26: System Design for FAANG Interviews
PART VIII — INTERVIEW STRATEGY
Chapter 27: Cracking FAANG — Strategy & Communication
20

Dynamic Programming


Chapter 20 ·  Part 4: ALGORITHMS ·  Est. 30 min read

Chapter Goal: Master Dynamic Programming — the most tested and most feared topic in FAANG interviews. DP is not about memorizing solutions; it is about developing an intuition for breaking problems into overlapping subproblems, defining states correctly, and writing the transition. By the end of this chapter you will have internalized every major DP pattern and be able to solve novel problems by recognizing which pattern applies.


20.1 DP Philosophy — What, Why & When

What Is Dynamic Programming?

DP is an optimization over plain recursion. When a recursive problem has overlapping subproblems, store the results to avoid recomputing them. The guarantee that the stored result is optimal comes from optimal substructure.

Two required conditions for DP:
1. OPTIMAL SUBSTRUCTURE: an optimal solution to the problem contains
   optimal solutions to its subproblems.
2. OVERLAPPING SUBPROBLEMS: the same subproblems are solved multiple times.

If only condition 1 holds → Greedy or Divide & Conquer
If both hold → DP (or Memoization)

Recognizing DP Problems

Signal words in the problem statement:
  "Find the minimum/maximum..."
  "Count the number of ways..."
  "Is it possible to..."
  "What is the length of the longest/shortest..."
  "Find an optimal..."

Key signals:
  → Choices at each step with consequences for future choices
  → Asking for a single number (not all possibilities) — if all, think backtracking
  → Constraints suggest O(n²) or O(n × W) is acceptable (n ≤ 1000)
  → Overlapping subproblems visible in the recursion tree

The "Last Step" Framework — How to Find the Recurrence

For any DP problem, ask: "What is the last decision made to reach this state?"

Climbing stairs: How do I reach step n?
  Last step was from step n-1 (took 1 step) OR from step n-2 (took 2 steps)
  → dp[n] = dp[n-1] + dp[n-2]

LCS of s and t: What's the last character matched?
  Case 1: s[i] == t[j] → both match → dp[i][j] = dp[i-1][j-1] + 1
  Case 2: don't use s[i] → dp[i][j] = dp[i-1][j]
  Case 3: don't use t[j] → dp[i][j] = dp[i][j-1]
  → dp[i][j] = dp[i-1][j-1]+1 if match, else max(dp[i-1][j], dp[i][j-1])

Coin Change: What's the last coin used to make amount A?
  Last coin is c (for each coin c in coins)
  → dp[A] = min over all c of (dp[A-c] + 1)

DP vs. Greedy vs. Divide & Conquer

DP Greedy D&C
Optimal substructure Yes Yes Yes
Overlapping subproblems Yes (non-overlapping) No
Makes local optimal choice (considers all) (only best) No
Direction Bottom-up or top-down Left-to-right Recursive split
Example Knapsack, LCS Activity Selection Merge Sort

20.2 Memoization — Top-Down DP

Start with a recursive solution, add a cache (memo) to store results.

General Pattern

unordered_map<State, Result> memo;
 
Result solve(State state) {
    // Base case
    if (isBase(state)) return baseResult(state);
 
    // Check cache
    if (memo.count(state)) return memo[state];
 
    // Compute and cache
    Result result = /* combine recursive calls */;
    return memo[state] = result;
}

Fibonacci — The Canonical Example

// Without memoization: O(2^n)
int fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);
}
 
// With memoization: O(n)
vector<int> memo(n+1, -1);
int fib(int n) {
    if (n <= 1) return n;
    if (memo[n] != -1) return memo[n];
    return memo[n] = fib(n-1) + fib(n-2);
}

When to Choose Top-Down

  • Problem is naturally recursive
  • Not all states need to be computed (sparse state space)
  • Easier to implement quickly in an interview
  • Complex state transitions that are hard to order iteratively

20.3 Tabulation — Bottom-Up DP

Build the DP table iteratively starting from base cases.

General Pattern

// 1. Define the DP array: dp[state] = answer for that state
// 2. Initialize base cases
// 3. Fill in larger states using smaller states
// 4. Return the answer (often dp[n] or dp[n][m])
 
vector<int> dp(n+1, 0);   // Or 2D: vector<vector<int>> dp(m+1, vector<int>(n+1, 0))
// Initialize base cases
dp[0] = 1;  // (varies by problem)
// Fill iteratively
for (int i = 1; i <= n; i++) {
    dp[i] = /* transition using dp[i-1], dp[i-2], ... */;
}
return dp[n];

Space Optimization — Rolling Array

Many DP problems only need the previous row/state. Save memory:

// 2D → 1D when dp[i][j] only depends on dp[i-1][...]
vector<int> dp(n+1, 0);   // Single array, reused each row
 
for (int i = 1; i <= m; i++) {
    for (int j = n; j >= 0; j--) {   // Reverse order for 0/1 knapsack!
        dp[j] = /* transition */;
    }
}
 
// 1D with two variables for Fibonacci-like:
int prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
    int curr = prev1 + prev2;
    prev2 = prev1;
    prev1 = curr;
}

When to Choose Bottom-Up

  • All states will be needed (dense state space)
  • Easier to optimize space
  • Faster in practice (no recursion overhead, better cache performance)
  • When the order of computation is clear

20.4 State Definition — The Most Critical Skill

The state is what completely describes the situation at a decision point. Wrong state → wrong recurrence → wrong answer.

How to Define States

Ask: "What information do I need to know to make the next decision?"

1. Position/index: dp[i] = answer for the first i elements
2. Remaining capacity: dp[w] = answer with capacity w left
3. Last choice: dp[i][prev] = answer at position i with prev as last choice
4. Range: dp[l][r] = answer for the subproblem on range [l, r]
5. Set membership: dp[mask] = answer for the subset represented by mask

Keep the state as SMALL as possible while capturing all necessary information.

State Definition Examples

Problem: Climbing Stairs (1 or 2 steps at a time)
  State: dp[i] = number of ways to reach step i
  Transition: dp[i] = dp[i-1] + dp[i-2]
  Why this works: to reach i, you came from i-1 or i-2

Problem: Coin Change (make amount A using coins)
  State: dp[a] = minimum coins to make amount a
  Transition: dp[a] = min over all coins c of (dp[a-c] + 1)
  Why this works: the last coin used determines the previous state

Problem: 0/1 Knapsack
  State: dp[i][w] = max value using first i items with capacity w
  Transition: dp[i][w] = max(dp[i-1][w], dp[i-1][w-wt[i]] + val[i])
  Why this works: for each item, either include or exclude it

Problem: Edit Distance (convert s to t)
  State: dp[i][j] = min edits to convert s[0..i-1] to t[0..j-1]
  Transition: see Section 20.8

20.5 1D DP — Linear State Problems

Climbing Stairs (LC 70)

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;
}
// Time: O(n), Space: O(1)

House Robber (LC 198)

Rob houses in a row; cannot rob two adjacent houses.

int rob(vector<int>& nums) {
    int n = nums.size();
    if (n == 1) return nums[0];
    int prev2 = nums[0], prev1 = max(nums[0], nums[1]);
    for (int i = 2; i < n; i++) {
        int curr = max(prev1, prev2 + nums[i]);
        prev2 = prev1; prev1 = curr;
    }
    return prev1;
}
// State: dp[i] = max money from houses 0..i
// Transition: dp[i] = max(dp[i-1], dp[i-2] + nums[i])
// Time: O(n), Space: O(1)

Decode Ways (LC 91)

Count ways to decode a digit string (1='A', ..., 26='Z').

int numDecodings(string s) {
    int n = s.size();
    // dp[i] = number of ways to decode s[0..i-1]
    vector<int> dp(n+1, 0);
    dp[0] = 1;                         // Empty string: 1 way
    dp[1] = s[0] != '0' ? 1 : 0;      // Single digit
 
    for (int i = 2; i <= n; i++) {
        int oneDigit = s[i-1] - '0';
        int twoDigit = stoi(s.substr(i-2, 2));
 
        if (oneDigit >= 1)                        // Valid single digit
            dp[i] += dp[i-1];
        if (twoDigit >= 10 && twoDigit <= 26)     // Valid double digit
            dp[i] += dp[i-2];
    }
    return dp[n];
}
// Time: O(n), Space: O(n) → optimizable to O(1)

Word Break (LC 139)

bool wordBreak(string s, vector<string>& wordDict) {
    unordered_set<string> dict(wordDict.begin(), wordDict.end());
    int n = s.size();
    vector<bool> dp(n+1, false);
    dp[0] = true;   // Empty string: can always be segmented
 
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j < i; j++) {
            if (dp[j] && dict.count(s.substr(j, i-j))) {
                dp[i] = true; break;
            }
        }
    }
    return dp[n];
}
// dp[i] = can s[0..i-1] be segmented using words in dict?
// Transition: dp[i] = OR over all j < i of (dp[j] && s[j..i-1] in dict)
// Time: O(n² × L) where L = avg word length, Space: O(n)

20.6 2D DP — Grid & Two-Sequence Problems

Unique Paths (LC 62)

int uniquePaths(int m, int n) {
    vector<vector<int>> dp(m, vector<int>(n, 1));  // First row/col = 1
    for (int i = 1; i < m; i++)
        for (int j = 1; j < n; j++)
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
    return dp[m-1][n-1];
}
// Space-optimized to O(n):
int uniquePaths(int m, int n) {
    vector<int> dp(n, 1);
    for (int i = 1; i < m; i++)
        for (int j = 1; j < n; j++)
            dp[j] += dp[j-1];
    return dp[n-1];
}
// Time: O(m×n), Space: O(n)

Minimum Path Sum (LC 64)

int minPathSum(vector<vector<int>>& grid) {
    int m = grid.size(), n = grid[0].size();
    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++) {
            if (i == 0 && j == 0) continue;
            else if (i == 0) grid[i][j] += grid[i][j-1];
            else if (j == 0) grid[i][j] += grid[i-1][j];
            else grid[i][j] += min(grid[i-1][j], grid[i][j-1]);
        }
    return grid[m-1][n-1];
}
// Modifies grid in-place. Time: O(m×n), Space: O(1)

Maximal Square (LC 221)

int maximalSquare(vector<vector<char>>& matrix) {
    int m = matrix.size(), n = matrix[0].size(), maxSide = 0;
    vector<vector<int>> dp(m, vector<int>(n, 0));
 
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (matrix[i][j] == '0') continue;
            if (i == 0 || j == 0) dp[i][j] = 1;
            else dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;
            maxSide = max(maxSide, dp[i][j]);
        }
    }
    return maxSide * maxSide;
}
// dp[i][j] = side length of largest square with bottom-right at (i,j)
// Key insight: limited by smallest of three neighbors
// Time: O(m×n), Space: O(m×n) → O(n) with rolling array

20.7 Knapsack Family

0/1 Knapsack — Each Item Used At Most Once

int knapsack01(int W, vector<int>& weights, vector<int>& values) {
    int n = weights.size();
    // dp[i][w] = max value using first i items with capacity w
    // dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i]] + values[i])
 
    // Space-optimized 1D (must traverse W in REVERSE to avoid reuse)
    vector<int> dp(W+1, 0);
    for (int i = 0; i < n; i++) {
        for (int w = W; w >= weights[i]; w--) {  // ← REVERSE order
            dp[w] = max(dp[w], dp[w-weights[i]] + values[i]);
        }
    }
    return dp[W];
}
// Time: O(n×W), Space: O(W)
// The REVERSE traversal ensures each item is used AT MOST ONCE

Unbounded Knapsack — Each Item Used Any Number of Times

int unboundedKnapsack(int W, vector<int>& weights, vector<int>& values) {
    vector<int> dp(W+1, 0);
    for (int i = 0; i < (int)weights.size(); i++) {
        for (int w = weights[i]; w <= W; w++) {  // ← FORWARD order
            dp[w] = max(dp[w], dp[w-weights[i]] + values[i]);
        }
    }
    return dp[W];
}
// The FORWARD traversal allows each item to be reused

Coin Change — Minimum Coins (Unbounded Knapsack Variant)

int coinChange(vector<int>& coins, int amount) {
    vector<int> dp(amount+1, amount+1);   // Initialize to impossible value
    dp[0] = 0;
 
    for (int a = 1; a <= amount; a++) {
        for (int coin : coins) {
            if (coin <= a) {
                dp[a] = min(dp[a], dp[a-coin] + 1);
            }
        }
    }
    return dp[amount] > amount ? -1 : dp[amount];
}
// Time: O(amount × coins), Space: O(amount)
 
// Alternative: iterate coins in outer loop (equivalent for unbounded)
for (int coin : coins)
    for (int a = coin; a <= amount; a++)
        dp[a] = min(dp[a], dp[a-coin] + 1);

Partition Equal Subset Sum (LC 416)

Can we partition an array into two subsets with equal sum? This is a 0/1 Knapsack: can we choose some elements that sum to total/2?

bool canPartition(vector<int>& nums) {
    int total = accumulate(nums.begin(), nums.end(), 0);
    if (total % 2 != 0) return false;
    int target = total / 2;
 
    vector<bool> dp(target+1, false);
    dp[0] = true;   // Can always make sum 0 (empty subset)
 
    for (int x : nums) {
        for (int j = target; j >= x; j--) {   // Reverse: each element used once
            dp[j] = dp[j] || dp[j-x];
        }
    }
    return dp[target];
}
// Time: O(n × target), Space: O(target)

Coin Change II — Count Ways (LC 518)

Count the number of ways to make amount using each coin unlimited times.

int change(int amount, vector<int>& coins) {
    vector<int> dp(amount+1, 0);
    dp[0] = 1;   // 1 way to make 0: use no coins
 
    for (int coin : coins) {             // Outer loop: COINS (ensures no duplicates)
        for (int a = coin; a <= amount; a++) {   // Inner: forward (unbounded)
            dp[a] += dp[a-coin];
        }
    }
    return dp[amount];
}
// NOTE: If we loop amount first then coins, we'd count permutations (not combinations)
// Coin outer loop ensures each combination is counted once
// Time: O(amount × coins), Space: O(amount)

20.8 String DP — LCS, Edit Distance & Palindromes

Longest Common Subsequence (LCS) — LC 1143

int longestCommonSubsequence(string s, string t) {
    int m = s.size(), n = t.size();
    // dp[i][j] = LCS length of s[0..i-1] and t[0..j-1]
    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 (s[i-1] == t[j-1])
                dp[i][j] = dp[i-1][j-1] + 1;    // Characters match
            else
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);  // Skip one char
        }
    }
    return dp[m][n];
}
// Time: O(m×n), Space: O(m×n) → O(n) with rolling array
 
/*
  s = "abcde", t = "ace"
  dp table:
       ""  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]  ← LCS = "ace" length 3
*/

Edit Distance (LC 72)

Minimum operations (insert, delete, replace) to convert s to t.

int minDistance(string s, string t) {
    int m = s.size(), n = t.size();
    // dp[i][j] = min edits to convert s[0..i-1] to t[0..j-1]
    vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
 
    // Base cases: convert to/from empty string
    for (int i = 0; i <= m; i++) dp[i][0] = i;  // Delete all s chars
    for (int j = 0; j <= n; j++) dp[0][j] = j;  // Insert all t chars
 
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (s[i-1] == t[j-1]) {
                dp[i][j] = dp[i-1][j-1];            // No operation needed
            } else {
                dp[i][j] = 1 + min({
                    dp[i-1][j],      // Delete s[i-1]
                    dp[i][j-1],      // Insert t[j-1]
                    dp[i-1][j-1]     // Replace s[i-1] with t[j-1]
                });
            }
        }
    }
    return dp[m][n];
}
// Time: O(m×n), Space: O(m×n)

Longest Palindromic Subsequence (LC 516)

int longestPalindromeSubseq(string s) {
    int n = s.size();
    // dp[i][j] = LPS length of s[i..j]
    vector<vector<int>> dp(n, vector<int>(n, 0));
    for (int i = 0; i < n; i++) dp[i][i] = 1;  // Single char
 
    for (int len = 2; len <= n; len++) {
        for (int i = 0; i + len - 1 < n; i++) {
            int j = i + len - 1;
            if (s[i] == s[j])
                dp[i][j] = dp[i+1][j-1] + 2;
            else
                dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
        }
    }
    return dp[0][n-1];
}
// Time: O(n²), Space: O(n²)

Wildcard Matching (LC 44)

bool isMatch(string s, string p) {
    int m = s.size(), n = p.size();
    // dp[i][j] = does s[0..i-1] match p[0..j-1]?
    vector<vector<bool>> dp(m+1, vector<bool>(n+1, false));
    dp[0][0] = true;   // Empty string matches empty pattern
 
    // p starting with '*' can match empty s
    for (int j = 1; j <= n; j++)
        if (p[j-1] == '*') dp[0][j] = dp[0][j-1];
 
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (p[j-1] == '*')
                dp[i][j] = dp[i-1][j]     // '*' matches one more char of s
                          || dp[i][j-1];   // '*' matches zero chars
            else if (p[j-1] == '?' || s[i-1] == p[j-1])
                dp[i][j] = dp[i-1][j-1];  // Current chars match
        }
    }
    return dp[m][n];
}
// Time: O(m×n), Space: O(m×n)

20.9 LIS — Longest Increasing Subsequence

O(n²) DP Solution

int lengthOfLIS(vector<int>& nums) {
    int n = nums.size();
    // dp[i] = length of LIS ending at index i
    vector<int> dp(n, 1);
 
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
    }
    return *max_element(dp.begin(), dp.end());
}
// Time: O(n²), Space: O(n)

O(n log n) Solution — Patience Sorting

Maintain an array tails where tails[k] = smallest tail of any LIS of length k+1. This array is always sorted, so we can binary search for insertion position.

int lengthOfLIS(vector<int>& nums) {
    vector<int> tails;  // tails[k] = smallest tail element of any IS of length k+1
 
    for (int x : nums) {
        // Find first position in tails where tails[pos] >= x
        int pos = lower_bound(tails.begin(), tails.end(), x) - tails.begin();
 
        if (pos == (int)tails.size())
            tails.push_back(x);   // x extends the longest IS
        else
            tails[pos] = x;       // x is a better (smaller) tail for IS of length pos+1
    }
    return tails.size();
}
// Time: O(n log n), Space: O(n)
 
/*
  nums = [10, 9, 2, 5, 3, 7, 101, 18]
  Process 10: tails=[10]
  Process 9:  find pos for 9 in [10] → pos=0, replace → tails=[9]
  Process 2:  pos=0, replace → tails=[2]
  Process 5:  pos=1, extend → tails=[2,5]
  Process 3:  pos=1, replace → tails=[2,3]
  Process 7:  pos=2, extend → tails=[2,3,7]
  Process 101: pos=3, extend → tails=[2,3,7,101]
  Process 18: pos=3, replace → tails=[2,3,7,18]
  Length = tails.size() = 4 ✓
*/

20.10 Interval DP

Interval DP solves problems over sub-intervals [l, r]. The outer dimension is the length of the interval.

Template

// dp[l][r] = answer for the subproblem on interval [l, r]
vector<vector<int>> dp(n, vector<int>(n, 0));
 
// Fill by increasing length
for (int len = 2; len <= n; len++) {           // Interval length
    for (int l = 0; l + len - 1 < n; l++) {   // Left endpoint
        int r = l + len - 1;                   // Right endpoint
        dp[l][r] = INITIAL_VALUE;
        for (int k = l; k < r; k++) {          // Split point / last step
            dp[l][r] = combine(dp[l][k], dp[k+1][r], /* k cost */);
        }
    }
}

Burst Balloons (LC 312)

Key insight: Instead of thinking "which balloon to burst first", think "which balloon to burst LAST" in the range (l, r). If balloon k is last, coins gained = nums[l-1] × nums[k] × nums[r+1] (since l-1 and r+1 are the remaining neighbors when k is popped).

int maxCoins(vector<int>& nums) {
    // Add sentinel balloons of value 1 at both ends
    nums.insert(nums.begin(), 1);
    nums.push_back(1);
    int n = nums.size();
 
    // dp[l][r] = max coins from bursting all balloons in open interval (l, r)
    vector<vector<int>> dp(n, vector<int>(n, 0));
 
    for (int len = 2; len < n; len++) {
        for (int l = 0; l + len < n; l++) {
            int r = l + len;
            for (int k = l+1; k < r; k++) {
                // k is the LAST balloon burst in (l, r)
                dp[l][r] = max(dp[l][r],
                    dp[l][k] + nums[l]*nums[k]*nums[r] + dp[k][r]);
            }
        }
    }
    return dp[0][n-1];
}
// Time: O(n³), Space: O(n²)

Palindrome Partitioning II (LC 132)

Minimum cuts to make every substring a palindrome.

int minCut(string s) {
    int n = s.size();
    // Precompute palindrome table
    vector<vector<bool>> isPalin(n, vector<bool>(n, false));
    for (int i = n-1; i >= 0; i--)
        for (int j = i; j < n; j++)
            isPalin[i][j] = (s[i]==s[j]) && (j-i<2 || isPalin[i+1][j-1]);
 
    // dp[i] = min cuts for s[0..i]
    vector<int> dp(n, 0);
    for (int i = 1; i < n; i++) {
        if (isPalin[0][i]) { dp[i] = 0; continue; }  // Whole prefix is palindrome
        dp[i] = i;   // Max cuts = i (cut every character)
        for (int j = 1; j <= i; j++) {
            if (isPalin[j][i]) dp[i] = min(dp[i], dp[j-1] + 1);
        }
    }
    return dp[n-1];
}
// Time: O(n²), Space: O(n²)

20.11 Tree DP

DP on trees processes subtrees bottom-up via DFS. Each node's answer depends on its children's answers.

House Robber III (LC 337)

Rob houses arranged as a binary tree. Cannot rob both a node and its parent.

// Returns {rob_this_node, skip_this_node}
pair<int,int> dfs(TreeNode* node) {
    if (!node) return {0, 0};
 
    auto [leftRob, leftSkip] = dfs(node->left);
    auto [rightRob, rightSkip] = dfs(node->right);
 
    // If we rob this node: cannot rob children
    int rob = node->val + leftSkip + rightSkip;
 
    // If we skip this node: children may or may not be robbed (take max)
    int skip = max(leftRob, leftSkip) + max(rightRob, rightSkip);
 
    return {rob, skip};
}
 
int rob(TreeNode* root) {
    auto [robRoot, skipRoot] = dfs(root);
    return max(robRoot, skipRoot);
}
// Time: O(n), Space: O(h) recursion stack

Binary Tree Maximum Path Sum — as Tree DP

// (Revisit from Ch10 with DP framing)
// dp[node] = max gain this node contributes going UPWARD (one direction)
// Update globalMax with node.val + leftGain + rightGain (can use both sides)
int maxPathSum(TreeNode* root) {
    int globalMax = INT_MIN;
    function<int(TreeNode*)> dp = [&](TreeNode* node) -> int {
        if (!node) return 0;
        int L = max(0, dp(node->left));
        int R = max(0, dp(node->right));
        globalMax = max(globalMax, node->val + L + R);
        return node->val + max(L, R);
    };
    dp(root);
    return globalMax;
}

20.12 Bitmask DP

Use a bitmask to represent which elements have been selected/visited. Applicable when n ≤ 20 (2^20 ≈ 10^6 states).

Traveling Salesman Problem (TSP)

// dp[mask][i] = min cost to visit all cities in mask, ending at city i
int tsp(int n, vector<vector<int>>& dist) {
    int FULL = (1 << n) - 1;
    vector<vector<int>> dp(1 << n, vector<int>(n, INT_MAX/2));
    dp[1][0] = 0;   // Start at city 0
 
    for (int mask = 1; mask < (1 << n); mask++) {
        for (int u = 0; u < n; u++) {
            if (!(mask & (1 << u))) continue;   // u must be in mask
            if (dp[mask][u] == INT_MAX/2) continue;
 
            for (int v = 0; v < n; v++) {
                if (mask & (1 << v)) continue;   // v must NOT be in mask
                int newMask = mask | (1 << v);
                dp[newMask][v] = min(dp[newMask][v], dp[mask][u] + dist[u][v]);
            }
        }
    }
 
    // Return to starting city (0) after visiting all
    int ans = INT_MAX;
    for (int u = 1; u < n; u++)
        if (dp[FULL][u] != INT_MAX/2)
            ans = min(ans, dp[FULL][u] + dist[u][0]);
    return ans;
}
// Time: O(2^n × n²), Space: O(2^n × n)

Count Shortest Superstring (LC 943)

Assemble all words into the shortest string where every word is a substring.

// dp[mask][i] = length of shortest superstring using words in mask, ending with words[i]
// Greedy: always extend with minimum overlap

20.13 State Machine DP — Stock Problems

A "state machine" tracks which discrete state you're in. Transitions between states depend on actions taken (buy, sell, hold, cooldown).

Best Time to Buy and Sell Stock (LC 121) — Single Transaction

int maxProfit(vector<int>& prices) {
    int minPrice = INT_MAX, maxProfit = 0;
    for (int p : prices) {
        minPrice  = min(minPrice, p);
        maxProfit = max(maxProfit, p - minPrice);
    }
    return maxProfit;
}

Best Time II (LC 122) — Unlimited Transactions

// State: hold (have stock), free (no stock)
int maxProfit(vector<int>& prices) {
    int hold = -prices[0], free = 0;
    for (int i = 1; i < (int)prices.size(); i++) {
        int newHold = max(hold, free - prices[i]);   // Hold or buy today
        int newFree = max(free, hold + prices[i]);   // Keep free or sell today
        hold = newHold; free = newFree;
    }
    return free;
}

Best Time with Cooldown (LC 309)

// States: hold, sold (just sold, cooldown), rest (can buy)
int maxProfit(vector<int>& prices) {
    int hold = -prices[0], sold = 0, rest = 0;
    for (int i = 1; i < (int)prices.size(); i++) {
        int prevHold = hold, prevSold = sold, prevRest = rest;
        hold = max(prevHold, prevRest - prices[i]);  // Hold or buy (from rest)
        sold = prevHold + prices[i];                  // Sell (must be in cooldown next)
        rest = max(prevRest, prevSold);               // Rest or come off cooldown
    }
    return max(sold, rest);
}

Best Time with Transaction Fee (LC 714)

int maxProfit(vector<int>& prices, int fee) {
    int hold = -prices[0], free = 0;
    for (int i = 1; i < (int)prices.size(); i++) {
        int newHold = max(hold, free - prices[i]);
        int newFree = max(free, hold + prices[i] - fee);  // Deduct fee on sell
        hold = newHold; free = newFree;
    }
    return free;
}

20.14 Common DP Anti-Patterns

Anti-Pattern 1 — Wrong State Definition

// ❌ State doesn't capture enough information
// Problem: max profit from stock with at most k transactions
int dp[n];   // dp[i] = max profit up to day i — WRONG: doesn't track transactions
 
// ✅ Correct state
int dp[n][k+1];  // dp[i][j] = max profit up to day i using j transactions

Anti-Pattern 2 — Wrong Traversal Order

// 0/1 Knapsack: each item used AT MOST once
// ❌ Forward traversal allows reuse
for (int w = 0; w <= W; w++) dp[w] = max(dp[w], dp[w-weight]+value);  // WRONG
 
// ✅ Reverse traversal prevents reuse
for (int w = W; w >= weight; w--) dp[w] = max(dp[w], dp[w-weight]+value);
 
// Unbounded Knapsack: item can be reused → forward traversal is correct!

Anti-Pattern 3 — Forgetting Base Cases

// Edit distance:
// ❌ Forgot to initialize first row/column
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
// First row: dp[0][j] = j (delete j chars to make empty)
// First col: dp[i][0] = i (insert i chars to match prefix)
// Without initialization → wrong answers for strings starting with empty

Anti-Pattern 4 — Off-by-One in Indices

// LCS with 1-indexed DP: dp[i][j] refers to s[i-1] and t[j-1]
if (s[i-1] == t[j-1])  // Correct: 1-indexed dp, 0-indexed strings
if (s[i]   == t[j])    // Wrong: if dp is 1-indexed

Anti-Pattern 5 — Integer Overflow in DP

// dp initialized to INT_MAX, then adding to it → overflow
int dp[W+1];
fill(dp, dp+W+1, INT_MAX);
dp[a] = dp[a-coin] + 1;  // ❌ If dp[a-coin] == INT_MAX → overflow!
 
// ✅ Use a large but safe sentinel
fill(dp, dp+W+1, amount+1);  // amount+1 > any valid answer → safe

20.15 Sample Problems


Problem 1 — Climbing Stairs (Easy) — LC 70

int climbStairs(int n) {
    if (n<=2) return n;
    int a=1,b=2;
    for(int i=3;i<=n;i++){int c=a+b;a=b;b=c;}
    return b;
}
// Time: O(n), Space: O(1)

Problem 2 — House Robber (Medium) — LC 198

int rob(vector<int>& nums) {
    int prev2=0,prev1=0;
    for(int x:nums){int curr=max(prev1,prev2+x);prev2=prev1;prev1=curr;}
    return prev1;
}
// Time: O(n), Space: O(1)

Problem 3 — Coin Change (Medium) — LC 322

int coinChange(vector<int>& coins, int amount) {
    vector<int> dp(amount+1, amount+1);
    dp[0]=0;
    for(int a=1;a<=amount;a++)
        for(int c:coins)
            if(c<=a) dp[a]=min(dp[a],dp[a-c]+1);
    return dp[amount]>amount?-1:dp[amount];
}
// Time: O(amount × coins), Space: O(amount)

Problem 4 — Unique Paths (Medium) — LC 62

int uniquePaths(int m, int n) {
    vector<int> dp(n,1);
    for(int i=1;i<m;i++) for(int j=1;j<n;j++) dp[j]+=dp[j-1];
    return dp[n-1];
}
// Time: O(m×n), Space: O(n)

Problem 5 — Longest Common Subsequence (Medium) — LC 1143

int longestCommonSubsequence(string s, string t) {
    int m=s.size(),n=t.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++)
        dp[i][j]=s[i-1]==t[j-1]?dp[i-1][j-1]+1:max(dp[i-1][j],dp[i][j-1]);
    return dp[m][n];
}
// Time: O(m×n), Space: O(m×n)

Problem 6 — Edit Distance (Medium) — LC 72

int minDistance(string s, string t) {
    int m=s.size(),n=t.size();
    vector<vector<int>> dp(m+1,vector<int>(n+1));
    for(int i=0;i<=m;i++) dp[i][0]=i;
    for(int j=0;j<=n;j++) dp[0][j]=j;
    for(int i=1;i<=m;i++) for(int j=1;j<=n;j++)
        dp[i][j]=s[i-1]==t[j-1]?dp[i-1][j-1]:1+min({dp[i-1][j],dp[i][j-1],dp[i-1][j-1]});
    return dp[m][n];
}
// Time: O(m×n), Space: O(m×n)

Problem 7 — Longest Increasing Subsequence (Medium) — LC 300

int lengthOfLIS(vector<int>& nums) {
    vector<int> tails;
    for(int x:nums){
        auto it=lower_bound(tails.begin(),tails.end(),x);
        if(it==tails.end()) tails.push_back(x);
        else *it=x;
    }
    return tails.size();
}
// Time: O(n log n), Space: O(n)

Problem 8 — Partition Equal Subset Sum (Medium) — LC 416

bool canPartition(vector<int>& nums) {
    int sum=accumulate(nums.begin(),nums.end(),0);
    if(sum%2) return false;
    int T=sum/2;
    vector<bool> dp(T+1,false);
    dp[0]=true;
    for(int x:nums) for(int j=T;j>=x;j--) dp[j]=dp[j]||dp[j-x];
    return dp[T];
}
// Time: O(n×T), Space: O(T)

Problem 9 — Burst Balloons (Hard) — LC 312

int maxCoins(vector<int>& nums) {
    nums.insert(nums.begin(),1); nums.push_back(1);
    int n=nums.size();
    vector<vector<int>> dp(n,vector<int>(n,0));
    for(int len=2;len<n;len++)
        for(int l=0;l+len<n;l++){
            int r=l+len;
            for(int k=l+1;k<r;k++)
                dp[l][r]=max(dp[l][r],dp[l][k]+nums[l]*nums[k]*nums[r]+dp[k][r]);
        }
    return dp[0][n-1];
}
// Time: O(n³), Space: O(n²)

Problem 10 — House Robber III (Medium) — LC 337

int rob(TreeNode* root) {
    function<pair<int,int>(TreeNode*)> dfs=[&](TreeNode* node)->pair<int,int>{
        if(!node) return {0,0};
        auto[lr,ls]=dfs(node->left);
        auto[rr,rs]=dfs(node->right);
        return {node->val+ls+rs, max(lr,ls)+max(rr,rs)};
    };
    auto[r,s]=dfs(root);
    return max(r,s);
}
// Time: O(n), Space: O(h)

Problem 11 — Word Break (Medium) — LC 139

bool wordBreak(string s, vector<string>& wordDict) {
    unordered_set<string> dict(wordDict.begin(),wordDict.end());
    int n=s.size();
    vector<bool> dp(n+1,false);
    dp[0]=true;
    for(int i=1;i<=n;i++)
        for(int j=0;j<i;j++)
            if(dp[j]&&dict.count(s.substr(j,i-j))){dp[i]=true;break;}
    return dp[n];
}
// Time: O(n² × L), Space: O(n)

Problem 12 — Best Time to Buy and Sell Stock with Cooldown (Medium) — LC 309

int maxProfit(vector<int>& prices) {
    int hold=INT_MIN, sold=0, rest=0;
    hold=-prices[0];
    for(int i=1;i<(int)prices.size();i++){
        int ph=hold,ps=sold,pr=rest;
        hold=max(ph,pr-prices[i]);
        sold=ph+prices[i];
        rest=max(pr,ps);
    }
    return max(sold,rest);
}
// Time: O(n), Space: O(1)

20.16 LeetCode Problem Set

# Problem Difficulty Core Pattern Link
1 Climbing Stairs Easy 1D DP (Fibonacci) LC 70
2 House Robber Medium 1D DP (adjacent constraint) LC 198
3 Coin Change Medium Unbounded Knapsack LC 322
4 Unique Paths Medium 2D DP (grid) LC 62
5 Longest Common Subsequence Medium String DP (two-sequence) LC 1143
6 Longest Increasing Subsequence Medium LIS (O(n log n)) LC 300
7 Edit Distance Medium String DP (three operations) LC 72
8 Burst Balloons Hard Interval DP (last burst) LC 312

Suggested order: #1 → #2 (warm up 1D DP). #3 (Coin Change — the canonical unbounded knapsack). #4 (2D grid DP). #5 (LCS — master string DP). #6 (LIS — both O(n²) and O(n log n)). #7 (Edit Distance — the hardest string DP). Save #8 (Burst Balloons) for last — it requires a completely non-obvious reformulation and is the benchmark interval DP problem.


← PreviousChapter 19: Graph Algorithms — Shortest Paths, MST & Union-FindNext →Chapter 21: Greedy Algorithms
Chapter 20 — Progress
0 / 36 COMPLETE
Philosophy & Recognition
  • State the two required conditions for DP: optimal substructure + overlapping subproblems
  • Recognize DP signal words: "minimum/maximum", "count ways", "is it possible"
  • Apply the "last step" framework to derive the recurrence for any DP
  • Know when to use top-down (memoization) vs. bottom-up (tabulation)
1D DP
  • Implement Climbing Stairs in O(n) time, O(1) space
  • Implement House Robber with rolling variables (no array needed)
  • Implement Decode Ways with the two-digit boundary check
  • Implement Word Break using DP with dictionary lookup
2D DP
  • Implement Unique Paths with 1D space-optimized DP
  • Implement Minimum Path Sum modifying grid in-place
  • Implement Maximal Square: dp[i][j] = min(left, top, diagonal) + 1
Knapsack Family
  • Know: 0/1 knapsack uses REVERSE traversal (each item used at most once)
  • Know: Unbounded knapsack uses FORWARD traversal (items reusable)
  • Know: Coin Change (count ways) — iterate coins in OUTER loop
  • Implement Partition Equal Subset Sum as a knapsack boolean DP
String DP
  • Implement LCS from memory with the two-case transition
  • Implement Edit Distance with all three operations (insert, delete, replace)
  • Implement Longest Palindromic Subsequence (LPS = LCS of s and reversed s)
  • Know the base cases for string DP: dp[i][0] = i, dp[0][j] = j
LIS
  • Implement O(n²) LIS: dp[i] = max over j<i where nums[j]<nums[i] of dp[j]+1
  • Implement O(n log n) LIS using patience sorting with lower_bound
  • Know: `tails` array is always sorted; lower_bound finds position to update
Interval DP
  • Fill interval DP by INCREASING LENGTH (not left-to-right directly)
  • Apply "last burst" insight to Burst Balloons
  • Implement Palindrome Partitioning II: O(n²) with precomputed palindrome table
Tree DP
  • Return a pair/struct from DFS: {include_root, exclude_root}
  • Implement House Robber III returning {rob, skip} from each subtree
State Machine DP
  • Implement stock problems with explicit state variables (hold, free, sold, rest)
  • Know: cooldown problem needs three states (hold, sold, rest)
  • Know: fee problem deducts fee on sell (in free state update)
Bitmask DP
  • Know: applicable when n ≤ 20 (2^20 ≈ 10^6 states)
  • State: dp[mask][last] = answer for subset mask ending at position last
Anti-Patterns
  • Avoid INT_MAX as initial value (overflows when adding 1) — use n+1 or amount+1
  • Always initialize base cases (row 0, column 0 for 2D DP)
  • Verify traversal order: 0/1 knapsack MUST go reverse
  • Ensure state captures all information needed for the transition
Notes▸