Dynamic Programming
1D DP | 2D DP | Knapsack | LCS | Intervals | State Machines
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
- STEP 1 β DEFINE THE STATE: What does
dp[i](ordp[i][j]) represent? Be precise. Write it in plain English first. - STEP 2 β WRITE THE RECURRENCE: How does
dp[i]relate to smaller subproblemsdp[i-1], dp[i-2], ...? This is the heart of the solution. - STEP 3 β IDENTIFY BASE CASES: What are the smallest subproblems with known answers? Fill these first to avoid out-of-bounds access.
- STEP 4 β DETERMINE FILL ORDER: Which order guarantees that when computing
dp[i], all requireddp[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] = 3Recurrence: 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 5i=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
4.2 β 1D DP: Coin Change
4.3 β 1D DP: Longest Increasing Subsequence (LIS)
// O(n log n) version using binary search + patience sorting
int lengthOfLIS_nlogn(vector
</div>
<h3>4.4 β 2D DP: Longest Common Subsequence</h3>
<div class="ch-code-wrap">
```cpp
// 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
// Unbounded Knapsack (items reusable): iterate capacity FORWARD
int unboundedKnapsack(vector
</div>
<h3>4.6 β State Machine DP: Stock Problems</h3>
<div class="ch-code-wrap">
```cpp
// 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
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 ondp[i-1]only β use two variables (O(n) β O(1)).dp[i]depends ondp[i-1]anddp[i-2]β use three rolling variables (O(n) β O(1)).dp[i][j]depends ondp[i-1][...]only β use two 1D arrays (O(nΒ²) β O(n)).dp[i][j]depends ondp[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)
6.2 β Unique Paths on Grid
// LC 63 β Unique Paths II (with obstacles)
int uniquePathsWithObstacles(vector<vector
</div>
<h3>6.3 β Word Break</h3>
<div class="ch-code-wrap">
```cpp
// 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 computingdp[7],dp[8],dp[9],dp[11], ... Memoisingdp[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+1as sentinel for 'unreachable'.
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];
}
};
</div>
<p><strong>Complexity:</strong> Time O(amount * n), Space O(amount)</p>
</section>
<!-- SECTION 9 -->
<section id="section-9" class="chapter-section">
<h2> 9 β Solved Problem 2: Longest Common Subsequence</h2>
<p>Given two strings <code>text1</code> and <code>text2</code>, 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.</p>
<div class="insight-box">
<h4>OBSERVATIONS</h4>
<ul>
<li>A subsequence is NOT a substring β characters don't need to be contiguous.</li>
<li>State definition: <code>dp[i][j]</code> = length of LCS of <code>text1[0..i-1]</code> and <code>text2[0..j-1]</code>. 1-indexed to simplify base cases.</li>
<li>Recurrence: If <code>text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1] + 1</code> (extend LCS by matching char). Else: <code>dp[i][j] = max(dp[i-1][j], dp[i][j-1])</code> (skip one char from either string).</li>
<li>Base cases: <code>dp[0][j] = 0</code> (empty text1) and <code>dp[i][0] = 0</code> (empty text2). Automatically initialised by vector constructor.</li>
<li>Fill order: row by row, left to right. <code>dp[i][j]</code> only depends on <code>dp[i-1][j-1]</code>, <code>dp[i-1][j]</code>, <code>dp[i][j-1]</code> β all already computed.</li>
</ul>
</div>
<div class="ch-code-wrap">
```cpp
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] = 0is critical. For LCS, the entiredp[0][j]anddp[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 exceptdp[0]=0.
10.3 β Edge Cases
- Empty string LCS: if either string is empty, LCS = 0. Base cases
dp[0][j]=dp[i][0]=0handle this. - No valid coin combination: return -1, not 0. Check if
dp[amount] > amountafter filling. amount = 0for coin change: always return 0 (zero coins needed).dp[0] = 0handles 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 ondp[i-1]anddp[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).