Interview Setbook · Part 5 of 6 · Q141–Q175
C++ Mastery Series · Interview Prep

The Interview
Problem Setbook

Part 5 of 6. Dynamic Programming — the hardest skill to teach and the most rewarding to master. Every problem follows the full structure: Problem → Decode → Approach Comparison → Dry Run Trace → Annotated C++ Code → Complexity Footer. No shortcuts.

Part 5 · Q141–Q175 1D DP 2D DP Knapsack Patterns String DP Interval DP DP on Trees
Chapter 14 · Q141–Q150 · 1D DP Foundations
1D DP Foundations — The Recurrences That Everything Else Builds On
The DP Framework — Apply This to Every New Problem

Step 1 — Define the subproblem: What does dp[i] mean? Define it in one precise English sentence. Never write code without this.
Step 2 — Write the recurrence: How does dp[i] depend on smaller subproblems? This is the DP transition.
Step 3 — Base cases: What are the smallest subproblems you know the answer to?
Step 4 — Order of computation: Which direction to fill the table? Ensure dependencies are already computed.
Step 5 — Extract answer: Which cell(s) contain the final answer?

Q141 · EASY Climbing Stairs
DPMath
LeetCode #70
Must KnowAsked 8,000+Fibonacci in Disguise

You can climb 1 or 2 steps at a time. How many distinct ways are there to reach the top of n stairs?

n = 2Output: 2 (1+1) or (2)
n = 3Output: 3 (1+1+1),(1+2),(2+1)
n = 5Output: 8
1 ≤ n ≤ 45
Decode — The Last Step Was Either 1 or 2; Sum Those Two Cases

To reach stair n, your last move was either a 1-step from stair n−1, or a 2-step from stair n−2. So the number of ways to reach n is the number of ways to reach n−1 plus the number of ways to reach n−2. That is the Fibonacci recurrence — every step's answer depends only on the previous two.

Brute O(2ⁿ)

Recursion

Recursively compute ways(n-1) + ways(n-2). Exponential — recomputes same subproblems.

Time: O(2ⁿ) · Space: O(n)
Memo O(n)

Top-Down

Same recursion with memoization array. Each subproblem solved once.

Time: O(n) · Space: O(n)
Optimal O(n)

Two Variables

Only need previous two values — no array needed. Roll forward like Fibonacci.

Time: O(n) · Space: O(1) ✓
Approach — Rolling Fibonacci

dp[i] = number of distinct ways to reach stair i. Transition: dp[i] = dp[i-1] + dp[i-2]. Base cases: dp[1]=1, dp[2]=2. Since each step only needs the last two values, replace the array with two variables a and b. At each step compute next = a + b, then shift: a = b, b = next.

n = 5 — rolling variables a, b Start: a=dp[1]=1, b=dp[2]=2 i=3: next=a+b=1+2=3 a=2, b=3 i=4: next=a+b=2+3=5 a=3, b=5 i=5: next=a+b=3+5=8 a=5, b=8 Answer = b = 8 (Matches: 1,2,3,5,8 — Fibonacci shifted by 1)
C++ Solution — O(n) time · O(1) space int climbStairs(int n) { if(n <= 2) return n; int a=1, b=2; for(int i=3; i<=n; i++){ int next = a + b; // dp[i] = dp[i-1] + dp[i-2] a = b; b = next; } return b; }
TimeO(n)
SpaceO(1)
Generalisationk steps allowed → sliding window sum of last k dp values
Q142 · EASY House Robber
DPArray
LeetCode #198
Must KnowAsked 7,000+Rob or Skip — Two Choices Per House

Houses in a row, each with a non-negative amount. Cannot rob two adjacent houses. Find the maximum amount you can rob.

Input[1,2,3,1]
Output4
WhyRob house 1(1)+3(3)=4
Input 2[2,7,9,3,1]
Output 212
Decode — At Each House, Two Choices: Rob It or Skip It

At house i you have exactly two choices. Rob it: gain nums[i] but cannot have robbed i−1, so best you had before is dp[i−2]. Skip it: gain nothing but carry forward the best up to dp[i−1]. The recurrence is dp[i] = max(dp[i−1], dp[i−2] + nums[i]). Only the last two values are ever needed.

Brute O(2ⁿ)

All Subsets

Try every subset of non-adjacent houses. Exponential.

Time: O(2ⁿ)
Optimal O(n)

Two Variables

prev2 = best up to i−2, prev1 = best up to i−1. At each step: curr = max(prev1, prev2 + nums[i]).

Time: O(n) · Space: O(1) ✓
Approach — Roll Two Variables Forward

dp[i] = max money robbing from houses 0..i. Transition: dp[i] = max(dp[i-1], dp[i-2] + nums[i]). Base: dp[0]=nums[0], dp[1]=max(nums[0],nums[1]). Space-optimise by keeping only prev2 and prev1.

nums = [2, 7, 9, 3, 1] prev2=0, prev1=0 i=0: curr=max(0, 0+2)=2 prev2=0, prev1=2 i=1: curr=max(2, 0+7)=7 prev2=2, prev1=7 i=2: curr=max(7, 2+9)=11 prev2=7, prev1=11 i=3: curr=max(11,7+3)=11 prev2=11, prev1=11 i=4: curr=max(11,11+1)=12 prev2=11, prev1=12 Answer = prev1 = 12 (rob houses 0,2,4 → 2+9+1=12)
C++ Solution — O(n) time · O(1) space int rob(vector<int>& nums) { int prev2=0, prev1=0; for(int x : nums){ int curr = max(prev1, prev2 + x); // Rob x or skip x prev2 = prev1; prev1 = curr; } return prev1; }
TimeO(n)
SpaceO(1)
Follow-upQ143 circular row · LC#337 tree structure
Q143 · MEDIUM House Robber II (Circular)
DPArray
LeetCode #213
Must KnowCircular = Two Linear Passes; Take the Max

Same as House Robber, but houses are in a circle — the first and last houses are adjacent. Cannot rob both.

Input[2,3,2]
Output3
Input 2[1,2,3,1]
Output 24
Decode — Break the Circle by Fixing Two Cases: Include First or Include Last

The first and last house can never both be robbed. So split into two independent linear problems: Case A: rob from house 0 to n−2 (exclude last). Case B: rob from house 1 to n−1 (exclude first). The answer is max(A, B). Each case is exactly Q142.

Brute

All Non-Adjacent Subsets

Exponential — try all valid subsets of a circular arrangement.

Time: O(2ⁿ)
Optimal O(n)

Two Linear Passes

Run robLinear(nums[0..n-2]) and robLinear(nums[1..n-1]). Return the max. Each pass is House Robber I.

Time: O(n) · Space: O(1) ✓
Approach — Two Calls to the Linear Helper

Write a helper robRange(lo, hi) that applies Q142's O(1) solution on the subarray. Call it twice with the two ranges. The trick of fixing two boundary cases is a general technique for turning circular problems into linear ones.

nums = [1, 2, 3, 1] (n=4) Case A: robRange(0, 2) = rob [1,2,3] i=1: curr=max(0,0+1)=1 prev2=0,prev1=1 i=2: curr=max(1,0+2)=2 prev2=1,prev1=2 i=3: curr=max(2,1+3)=4 → Case A = 4 Case B: robRange(1, 3) = rob [2,3,1] i=2: curr=max(0,0+2)=2 prev2=0,prev1=2 i=3: curr=max(2,0+3)=3 prev2=2,prev1=3 i=4: curr=max(3,2+1)=3 → Case B = 3 Answer = max(4,3) = 4
C++ Solution — O(n) time · O(1) space int robRange(vector<int>& nums, int lo, int hi){ int prev2=0, prev1=0; for(int i=lo; i<=hi; i++){ int curr = max(prev1, prev2 + nums[i]); prev2=prev1; prev1=curr; } return prev1; } int rob(vector<int>& nums){ int n=nums.size(); if(n==1) return nums[0]; return max(robRange(nums,0,n-2), // Exclude last robRange(nums,1,n-1)); // Exclude first }
TimeO(n)
SpaceO(1)
PatternCircular → split into two linear cases. Reusable for many circular problems.
Q144 · MEDIUM Jump Game I — Can You Reach the End?
DPGreedyArray
LeetCode #55
Must KnowAsked 6,000+Greedy Beats DP Here

Each element nums[i] is the maximum jump length from position i. Starting at index 0, return true if you can reach the last index.

Input[2,3,1,1,4]
Outputtrue
Input 2[3,2,1,0,4]
Output 2false
Decode — Track the Farthest Index Reachable So Far

At each index i, you can jump to at most i + nums[i]. The key question: as you walk from left to right, does the farthest reachable index ever reach n−1? If at any index i your current farthest reach is less than i itself, you're stranded.

DP O(n²)

Reachable Array

dp[i]=true if position i is reachable. For each reachable i, mark all positions i+1..i+nums[i] as reachable.

Time: O(n²)
Greedy O(n)

Max Reach

Walk left to right. Maintain maxReach. At each i: if i > maxReach → stranded. Else update maxReach = max(maxReach, i+nums[i]).

Time: O(n) · Space: O(1) ✓
Approach — Single Pass, Track Max Reachable Index

One variable: maxReach. Walk i from 0 to n−1. If i exceeds maxReach, return false — we're stranded. Otherwise update maxReach = max(maxReach, i + nums[i]). If we finish the loop, return true.

nums = [3, 2, 1, 0, 4] maxReach=0 i=0: 0 ≤ maxReach=0 ✓ maxReach=max(0,0+3)=3 i=1: 1 ≤ maxReach=3 ✓ maxReach=max(3,1+2)=3 i=2: 2 ≤ maxReach=3 ✓ maxReach=max(3,2+1)=3 i=3: 3 ≤ maxReach=3 ✓ maxReach=max(3,3+0)=3 i=4: 4 > maxReach=3 → STRANDED → return false Answer: false ✓
C++ Solution — O(n) time · O(1) space bool canJump(vector<int>& nums){ int maxReach=0; for(int i=0; i<(int)nums.size(); i++){ if(i > maxReach) return false; // Can't reach index i maxReach = max(maxReach, i + nums[i]); // Update farthest reachable } return true; }
TimeO(n)
SpaceO(1)
Follow-upQ145 Jump Game II — minimum jumps to reach end
Q145 · MEDIUM Jump Game II — Minimum Jumps
DPGreedyArray
LeetCode #45
Must KnowBFS Levels: Each Level = One Jump

Same setup as Jump Game I — guaranteed reachable. Return the minimum number of jumps to reach the last index.

Input[2,3,1,1,4]
Output2
Why0→1→4 (jump to index 1 then 4)
Input 2[2,3,0,1,4]
Output 22
Decode — Each Jump is a BFS Level; Extend the Window Greedily

Think of this as BFS levels. All indices reachable in exactly k jumps form "level k." From each position in level k, we can reach a new range of positions — forming level k+1. We don't need to track individual positions: just track the farthest index reachable at the current level and the farthest reachable at the next level.

DP O(n²)

dp[i] = min jumps to i

For each i, scan all j < i where j is reachable and j+nums[j] ≥ i.

Time: O(n²)
Greedy O(n)

BFS Window

Track curEnd (end of current jump's reach) and farthest. When i hits curEnd, increment jumps and extend window to farthest.

Time: O(n) · Space: O(1) ✓
Approach — Greedy Window Expansion

Three variables: jumps (count), curEnd (boundary of current jump's reach), farthest (max reachable from anything in current window). Walk i from 0 to n−2. Always update farthest. When i == curEnd: we must jump — increment jumps, set curEnd = farthest. Stop at n−2 because reaching n−1 during a window already counts.

nums = [2, 3, 1, 1, 4] jumps=0, curEnd=0, farthest=0 i=0: farthest=max(0,0+2)=2. i==curEnd=0 → jump! jumps=1, curEnd=2 i=1: farthest=max(2,1+3)=4 i=2: farthest=max(4,2+1)=4. i==curEnd=2 → jump! jumps=2, curEnd=4 i=3: (i < n-1=4, continue) farthest=max(4,3+1)=4 i=4: i == n-1, stop loop Answer = 2
C++ Solution — O(n) time · O(1) space int jump(vector<int>& nums){ int jumps=0, curEnd=0, farthest=0; for(int i=0; i<(int)nums.size()-1; i++){ farthest = max(farthest, i + nums[i]); if(i == curEnd){ // Exhausted current jump's window jumps++; curEnd = farthest; // Extend to best reachable } } return jumps; }
TimeO(n)
SpaceO(1)
PatternGreedy BFS window — same idea as interval scheduling
Q146 · MEDIUM Min Cost Climbing Stairs
DPArray
LeetCode #746
Must KnowQ141 with Costs Added

cost[i] = cost to step from stair i. You can start from index 0 or 1. From each stair you can climb 1 or 2 steps. Find the minimum cost to reach the top (past the last index).

Input[10,15,20]
Output15
Input 2[1,100,1,1,1,100,1,1,100,1]
Output 26
Decode — dp[i] = Min Cost to Reach Stair i

To reach stair i you came from either i−1 or i−2, paying their cost. So dp[i] = cost[i] + min(dp[i−1], dp[i−2]). The "top" is one step past the last index — the answer is min(dp[n−1], dp[n−2]).

Optimal O(n)

Rolling Two Variables

dp[i] = cost[i] + min(dp[i-1], dp[i-2]). Keep only last two values.

Time: O(n) · Space: O(1) ✓
Approach — Same Rolling Pattern as Q141/Q142

Set a=cost[0], b=cost[1]. For i from 2 to n−1: curr = cost[i] + min(a, b). Shift: a=b, b=curr. Answer = min(a, b) — the minimum cost to step from either of the last two stairs to the top.

cost = [10, 15, 20] a=10, b=15 i=2: curr=cost[2]+min(10,15)=20+10=30 a=15, b=30 Answer = min(a,b) = min(15,30) = 15 (Take stair 1 directly to top: cost=15)
C++ Solution — O(n) time · O(1) space int minCostClimbingStairs(vector<int>& cost){ int n=cost.size(), a=cost[0], b=cost[1]; for(int i=2; i<n; i++){ int curr=cost[i]+min(a,b); a=b; b=curr; } return min(a,b); }
TimeO(n)
SpaceO(1)
Q147 · MEDIUM Decode Ways
DPString
LeetCode #91
Must KnowAsked 5,000+Climbing Stairs With Validity Checks

A message encoded as digits: 'A'=1, 'B'=2, ..., 'Z'=26. Count the number of ways to decode a digit string.

Input"12"
Output2
Why"AB"(1,2) or "L"(12)
Input 2"226"
Output 23
Input 3"06"
Output 30
Decode — Climbing Stairs But Each Step Has Validity Conditions

Like climbing stairs (1 or 2 steps), but with rules: a 1-digit step is valid only if the digit is not '0'. A 2-digit step is valid only if the two-digit number is between 10 and 26. If neither is valid, dp[i]=0 (dead end). If only one is valid, dp[i] inherits from that one. If both valid, dp[i] = dp[i-1] + dp[i-2].

Optimal O(n)

Rolling DP

dp[i] = ways to decode s[0..i-1]. One-digit: s[i-1]≠'0' → add dp[i-1]. Two-digit: s[i-2..i-1] in [10,26] → add dp[i-2].

Time: O(n) · Space: O(1) ✓
Approach — Validate Both 1-Digit and 2-Digit Decodings

Use two variables: prev2 (dp[i-2]) and prev1 (dp[i-1]). At position i (1-indexed): check if s[i-1] alone is valid (not '0') and add prev1. Check if s[i-2..i-1] as two digits is in [10,26] and add prev2. Base: dp[0]=1 (empty string one way), dp[1]=1 if s[0]≠'0' else 0.

s = "226" prev2=1(dp[0]), prev1=1(dp[1]='2' valid) i=2 (char='2'): 1-digit: s[1]='2' ≠ '0' → valid → curr += prev1=1 2-digit: s[0..1]="22" = 22 ∈ [10,26] → valid → curr += prev2=1 curr = 2 prev2=1, prev1=2 i=3 (char='6'): 1-digit: s[2]='6' ≠ '0' → valid → curr += prev1=2 2-digit: s[1..2]="26" = 26 ∈ [10,26] → valid → curr += prev2=1 curr = 3 prev2=2, prev1=3 Answer = prev1 = 3
C++ Solution — O(n) time · O(1) space int numDecodings(string s){ int n=s.size(); if(s[0]=='0') return 0; int prev2=1, prev1=1; for(int i=2; i<=n; i++){ int curr=0; if(s[i-1]!='0') // Valid 1-digit decode curr += prev1; int two=(s[i-2]-'0')*10+(s[i-1]-'0'); if(two>=10 && two<=26) // Valid 2-digit decode curr += prev2; prev2=prev1; prev1=curr; } return prev1; }
TimeO(n)
SpaceO(1)
SimilarDecode Ways II (LC#639) — '*' wildcard adds cases
Q148 · MEDIUM Coin Change — Fewest Coins
DPArray
LeetCode #322
Must KnowAsked 6,000+Unbounded Knapsack Foundation

Given coin denominations and a target amount, find the fewest number of coins that sum to the amount. Return -1 if impossible. Unlimited supply of each coin.

coins[1,5,6,9]
amount11
Output2
Why5+6=11, 2 coins
Decode — For Each Amount, Try Every Coin as the Last Coin Used

dp[a] = fewest coins to make amount a. For each amount a, try using each coin c: if a−c is reachable, then dp[a] = min(dp[a], dp[a−c] + 1). The "+1" accounts for the coin c we just used. Base: dp[0]=0 (zero coins to make amount 0). All others start at ∞.

Brute

Recursion

Try all combinations of coins. Exponential, recomputes subproblems.

Time: O(S^n)
Optimal O(S×n)

Bottom-Up DP

Build dp[0..amount]. For each amount, try all coins. O(amount × #coins).

Time: O(S×n) · Space: O(S) ✓
Approach — Bottom-Up Table, Fill Amount by Amount

Create dp of size amount+1 initialised to ∞ (INT_MAX/2 to avoid overflow). dp[0]=0. For a from 1 to amount: for each coin c: if c ≤ a and dp[a-c]+1 < dp[a], update dp[a]. Answer: dp[amount] == ∞ ? -1 : dp[amount].

coins=[1,5,6,9], amount=11 (show key cells) dp[0]=0 dp[1]=dp[0]+1=1 (coin 1) dp[2]=dp[1]+1=2 (coin 1) dp[3]=3, dp[4]=4 dp[5]=min(dp[4]+1, dp[0]+1)=1 (coin 5) dp[6]=min(dp[5]+1, dp[0]+1)=1 (coin 6) dp[7]=min(dp[6]+1, dp[1]+1)=2 (6+1 or 5+1+1) ... dp[10]=min(dp[9]+1,dp[4]+1,dp[1]+1)=2 (coin9+1 or 5+5) dp[11]=min(dp[10]+1, dp[5]+1, dp[6]+1, ...) =min(3, dp[5]+1=2, dp[6]+1=2, ...)=2 (5+6) Answer: dp[11] = 2
C++ Solution — O(S×n) time · O(S) space int coinChange(vector<int>& coins, int amount){ vector<int> dp(amount+1, INT_MAX/2); 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); // Use coin c last return dp[amount]==INT_MAX/2 ? -1 : dp[amount]; }
TimeO(S × n)
SpaceO(S)
SimilarQ149 Coin Change II (count ways) · Perfect Squares (LC#279)
Q149 · MEDIUM Coin Change II — Count Ways
DPArray
LeetCode #518
Must KnowOrder of Loops Matters: Coins Outer, Amounts Inner = No Duplicates

Count the number of combinations (not permutations) of coins that sum to the target amount. Unlimited coins of each denomination.

coins[1,2,5]
amount5
Output4
Ways5, 2+2+1, 2+1+1+1, 1+1+1+1+1
Decode — Combinations Not Permutations: Fix Coin Order by Iterating Coins in Outer Loop

The critical insight: if you iterate coins in the outer loop and amounts in the inner loop, each coin is "introduced" one at a time — so {1,2} and {2,1} are only counted once. This is the classic unbounded knapsack counting pattern.

Amounts outer

Counts Permutations

Outer=amounts, inner=coins → {1,2} and {2,1} counted separately. Wrong for combinations.

Gives permutations
Coins outer ✓

Counts Combinations

Outer=coins, inner=amounts → each coin considered once across all amounts. No duplicates.

Time: O(S×n) · Space: O(S) ✓
Approach — Unbounded Knapsack Counting Pattern

dp[a] = number of ways to make amount a. Base: dp[0]=1. For each coin c (outer), for each amount a from c to amount (inner): dp[a] += dp[a-c]. After processing coin c, dp[a] only contains ways using coins seen so far — preventing duplicate combinations.

coins=[1,2,5], amount=5 dp=[1,0,0,0,0,0] Process coin=1: a=1: dp[1]+=dp[0]=1 → [1,1,0,0,0,0] a=2: dp[2]+=dp[1]=1 → [1,1,1,0,0,0] a=3: dp[3]+=dp[2]=1 → [1,1,1,1,0,0] a=4..5 similarly → [1,1,1,1,1,1] Process coin=2: a=2: dp[2]+=dp[0]=1 → dp[2]=2 (1+1 or 2) a=3: dp[3]+=dp[1]=1 → dp[3]=2 a=4: dp[4]+=dp[2]=2 → dp[4]=3 a=5: dp[5]+=dp[3]=2 → dp[5]=3 Process coin=5: a=5: dp[5]+=dp[0]=1 → dp[5]=4 Answer = dp[5] = 4
C++ Solution — O(S×n) time · O(S) space int change(int amount, vector<int>& coins){ vector<long> dp(amount+1, 0); dp[0]=1; // One way to make amount 0: use no coins for(int c : coins) // Coins OUTER → combinations for(int a=c; a<=amount; a++) dp[a] += dp[a-c]; return dp[amount]; }
TimeO(S × n)
SpaceO(S)
Key RuleCombinations = coins outer. Permutations = amounts outer.
Q150 · MEDIUM Longest Increasing Subsequence (LIS)
DPArray
LeetCode #300
Must KnowAsked 6,000+O(n²) DP then O(n log n) Patience Sort

Return the length of the longest strictly increasing subsequence (elements don't need to be adjacent).

Input[10,9,2,5,3,7,101,18]
Output4
LIS[2,3,7,101] or [2,5,7,101]
Input 2[0,1,0,3,2,3]
Output 24
Decode — dp[i] = Length of LIS Ending at Index i

For each index i, the LIS ending at i can be extended from any index j < i where nums[j] < nums[i]. So dp[i] = 1 + max(dp[j]) for all j < i where nums[j] < nums[i]. Base: dp[i]=1 (every element forms a length-1 subsequence by itself). Answer = max(dp[i]).

DP O(n²)

dp[i] per element

For each i, scan all j < i with nums[j] < nums[i], take max dp[j]+1. Clear and simple.

Time: O(n²) · Space: O(n)
O(n log n)

Patience Sorting

Maintain a tails array. Binary search for position of each element. Length of tails = LIS length.

Time: O(n log n) · Space: O(n) ✓
Approach A — O(n²) DP and B — O(n log n) Patience Sort

Patience Sort: Maintain array tails where tails[k] = the smallest tail element of all increasing subsequences of length k+1. For each num: binary search for the leftmost position in tails where tails[pos] ≥ num. Replace tails[pos] with num (or append if past end). The length of tails is the LIS length.

nums = [10, 9, 2, 5, 3, 7, 101, 18] (O(n²) DP) dp = [ 1, 1, 1, ?, ?, ?, ?, ?] i=3 (5): j=2: 2<5 → dp[3]=dp[2]+1=2 i=4 (3): j=2: 2<3 → dp[4]=dp[2]+1=2 i=5 (7): j=2: dp=2, j=3: 5<7 dp=3, j=4: 3<7 dp=3 → dp[5]=3 i=6 (101): all j < 6 are smaller → dp[6]=dp[5]+1=4 i=7 (18): j=5: 7<18 → dp[7]=4; 18<101 so dp[7]=4 max(dp) = 4 Patience Sort trace: num=10: tails=[10] num=9: replace 10 → tails=[9] num=2: replace 9 → tails=[2] num=5: append → tails=[2,5] num=3: replace 5 → tails=[2,3] num=7: append → tails=[2,3,7] num=101:append → tails=[2,3,7,101] num=18: replace 101→ tails=[2,3,7,18] len(tails) = 4
C++ — O(n²) DP and O(n log n) Patience Sort // O(n²) — intuitive, easier to understand int lisDP(vector<int>& nums){ int n=nums.size(); 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()); } // O(n log n) — patience sorting 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); // Extend else *it=x; // Replace to keep smallest tail } return tails.size(); }
DPO(n²)
PatienceO(n log n)
Built-inlower_bound on tails implements the binary search cleanly
↑ back to top
Chapter 15 · Q151–Q158 · Knapsack Patterns
Knapsack Patterns — 0/1, Unbounded, Partition & Multi-Dimensional
The Knapsack Family — Know These Four Variants

0/1 Knapsack: Each item used at most once. Inner loop goes RIGHT to LEFT to prevent re-using. Or use 2D dp[i][w].
Unbounded Knapsack: Each item used unlimited times. Inner loop goes LEFT to RIGHT.
Bounded Knapsack: Each item has a limited count. Split or binary grouping.
Partition / Subset Sum: Special case of 0/1 — can you reach exactly target weight? dp is boolean or count.

Q151 · MEDIUM 0/1 Knapsack
DPArray
GFG / CSES Classic
Must KnowTemplate for Every Knapsack Variant

n items each with weight w[i] and value v[i]. Knapsack capacity W. Maximise total value without exceeding capacity. Each item can be used at most once.

Weights[2,3,4,5]
Values[3,4,5,6]
W=5Output: 7
Whyitems 0+1 → w=5,v=7
Decode — For Each Item, Decide: Include It or Skip It

dp[i][w] = max value using items 0..i−1 with capacity w. At item i: skip it → dp[i−1][w]. include it (if w[i] ≤ w) → v[i] + dp[i−1][w−w[i]]. Space-optimise to 1D by iterating capacity RIGHT to LEFT (prevents an item from being used twice).

2D DP O(nW)

dp[i][w]

Table of n×W. Clear to understand. dp[i][w] = max(dp[i-1][w], v[i]+dp[i-1][w-w[i]]).

Time: O(nW) · Space: O(nW)
1D DP O(W)

Reverse inner loop

dp[w] updated right-to-left ensures item i can only be used once per row.

Time: O(nW) · Space: O(W) ✓
Approach — 1D DP, Inner Loop Reversed

Initialise dp[0..W] = 0. For each item i: loop w from W down to w[i]: dp[w] = max(dp[w], dp[w−w[i]] + v[i]). The right-to-left order means when we compute dp[w], dp[w−w[i]] still reflects the state WITHOUT item i — preventing reuse.

w=[2,3], v=[3,4], W=5 dp=[0,0,0,0,0,0] Item 0 (weight=2,value=3): loop w=5..2 w=5: dp[5]=max(0,dp[3]+3)=max(0,0+3)=3 w=4: dp[4]=max(0,dp[2]+3)=3 w=3: dp[3]=max(0,dp[1]+3)=3 w=2: dp[2]=max(0,dp[0]+3)=3 dp=[0,0,3,3,3,3] Item 1 (weight=3,value=4): loop w=5..3 w=5: dp[5]=max(3,dp[2]+4)=max(3,3+4)=7 w=4: dp[4]=max(3,dp[1]+4)=3+4? No, dp[1]=0 → max(3,4)=4? Wait: dp[4]=max(dp[4], dp[4-3]+4)=max(3,dp[1]+4)=max(3,0+4)=4 w=3: dp[3]=max(3,dp[0]+4)=max(3,4)=4 dp=[0,0,3,4,4,7] Answer = dp[5] = 7
C++ Solution — O(nW) time · O(W) space int knapsack01(int W, vector<int>& wt, vector<int>& val){ int n=wt.size(); vector<int> dp(W+1, 0); for(int i=0; i<n; i++) for(int w=W; w>=wt[i]; w--) // RIGHT TO LEFT — each item used once dp[w] = max(dp[w], dp[w-wt[i]] + val[i]); return dp[W]; }
TimeO(nW)
SpaceO(W)
CriticalRight-to-left = 0/1. Left-to-right = unbounded. Memorise this.
Q152 · MEDIUM Partition Equal Subset Sum
DPArray
LeetCode #416
Must KnowAsked 5,000+Subset Sum = 0/1 Knapsack Boolean

Can you partition an array into two subsets with equal sum? Return true/false.

Input[1,5,11,5]
Outputtrue
Why[1,5,5] and [11]
Input 2[1,2,3,5]
Output 2false
Decode — Equal Partition ↔ Subset Summing to total/2

If total is odd → impossible immediately. If even, the question reduces to: can any subset sum to total/2? This is the 0/1 Knapsack boolean variant: dp[s] = true if some subset of seen items sums exactly to s. Same right-to-left iteration as Q151.

0/1 Knapsack Bool

dp[s] = reachable?

dp is boolean. dp[0]=true. For each num: loop s from target down to num. dp[s] |= dp[s-num].

Time: O(n×S) · Space: O(S) ✓
Approach — Boolean Subset-Sum Knapsack

target = sum/2. dp[0..target] boolean, dp[0]=true. For each num (right-to-left): dp[s] |= dp[s−num]. If dp[target] becomes true at any point, return early. After all nums: return dp[target].

nums=[1,5,11,5], sum=22, target=11 dp=[T,F,F,F,F,F,F,F,F,F,F,F] num=1: s=11..1: dp[1]|=dp[0]=T → dp[1]=T dp=[T,T,F,F,F,F,F,F,F,F,F,F] num=5: s=11..5: dp[6]|=dp[1]=T → dp[6]=T dp[5]|=dp[0]=T → dp[5]=T dp=[T,T,F,F,F,T,T,F,F,F,F,F] num=11: s=11..11: dp[11]|=dp[0]=T → dp[11]=T → return true!
C++ Solution — O(n×S) time · O(S) space bool canPartition(vector<int>& nums){ int total=accumulate(nums.begin(),nums.end(),0); if(total%2) return false; int target=total/2; vector<bool> dp(target+1,false); dp[0]=true; for(int x : nums) for(int s=target; s>=x; s--) // Right-to-left: each item used once dp[s]=dp[s]||dp[s-x]; return dp[target]; }
TimeO(n × sum/2)
SpaceO(sum/2)
RelatedTarget Sum (LC#494) · Last Stone Weight II (LC#1049)
Q153 · MEDIUM Target Sum — Count Assignments
DPArray
LeetCode #494
Must KnowAdd + or - to Each Number, Count Ways = Subset Sum Count

Assign + or − to each number. Count ways to make the total equal to target.

nums[1,1,1,1,1]
target3
Output5
E.g.-1+1+1+1+1=3
Decode — Reduce to Subset Sum Count via Algebra

Let P = sum of positive-assigned numbers. Let N = sum of negative-assigned. Then P + N = total, P − N = target. Solving: P = (total + target) / 2. So the question becomes: how many subsets sum to (total+target)/2? This is the counting variant of subset sum — 0/1 knapsack where dp[s] = number of ways.

Approach — Count Subsets Summing to (total+target)/2

If (total+target) is odd or target > total → 0 ways. Otherwise target_sum = (total+target)/2. dp[0..target_sum] counting, dp[0]=1. For each num (right-to-left): dp[s] += dp[s−num]. Return dp[target_sum].

nums=[1,1,1,1,1], target=3, total=5 target_sum=(5+3)/2=4 dp=[1,0,0,0,0] num=1: s=4..1: dp[4]+=dp[3]=0, dp[3]+=dp[2]=0, dp[2]+=dp[1]=0, dp[1]+=dp[0]=1 dp=[1,1,0,0,0] num=1: s=4..1: dp[2]+=dp[1]=1→dp[2]=1, dp[1]+=dp[0]=1→dp[1]=2 dp=[1,2,1,0,0] num=1 (×3 more times, each pass doubles patterns up): After all 5 nums: dp=[1,5,10,10,5] Answer = dp[4] = 5
C++ Solution — O(n×S) time · O(S) space int findTargetSumWays(vector<int>& nums, int target){ int total=accumulate(nums.begin(),nums.end(),0); if((total+target)%2 || abs(target)>total) return 0; int S=(total+target)/2; vector<int> dp(S+1,0); dp[0]=1; for(int x:nums) for(int s=S;s>=x;s--) dp[s]+=dp[s-x]; return dp[S]; }
TimeO(n × S)
SpaceO(S)
Q154 · MEDIUM Last Stone Weight II
DPArray
LeetCode #1049
Assign +/− to Each Stone, Minimise Absolute Difference

Smash stones together. When two stones collide, the difference remains. Minimise the smallest possible remaining weight. (Each stone gets a + or − sign; minimise |sum|.)

Input[2,7,4,1,8,1]
Output1
Decode — Minimise |P − N| = Minimise |total − 2P| = Maximise P ≤ total/2

Assign + to some stones (set P) and − to others (set N). Result = |P−N| = |total−2P|. To minimise this, maximise P subject to P ≤ total/2. This is a 0/1 knapsack maximisation where capacity = total/2.

Approach — 0/1 Knapsack Max Sum ≤ total/2

target = total/2. dp[s] = max achievable sum using subset ≤ s. Same as Q151 but maximising. dp[0]=0. For each stone (right-to-left): dp[s] = max(dp[s], dp[s−stone]+stone). Answer = total − 2×dp[target].

stones=[2,7,4,1,8,1] total=23, target=11 After 0/1 knapsack on capacity 11: Best subset summing to ≤ 11: {2,1,8}=11 or {2,4,1,1}=8? Actually {2,4,1,1,1}... let me trace key steps: dp[11]=11 (achievable: e.g. 2+1+8=11) Answer = total - 2×dp[11] = 23 - 2×11 = 1
C++ Solution — O(n×S) time · O(S) space int lastStoneWeightII(vector<int>& stones){ int total=accumulate(stones.begin(),stones.end(),0); int target=total/2; vector<int> dp(target+1,0); for(int s:stones) for(int w=target;w>=s;w--) dp[w]=max(dp[w],dp[w-s]+s); return total-2*dp[target]; }
TimeO(n × total/2)
SpaceO(total/2)
Q155 · MEDIUM Word Break
DPString
LeetCode #139
Must KnowAsked 6,000+Unbounded Knapsack on Strings

Given a string s and a dictionary of words, can s be segmented into a space-separated sequence of dictionary words?

s"leetcode"
dict["leet","code"]
Outputtrue
s 2"applepenapple"
dict 2["apple","pen"]
Output 2true
Decode — dp[i] = Can s[0..i-1] Be Fully Segmented?

dp[i] = true if the first i characters of s can be broken into dictionary words. Transition: dp[i] is true if there exists some j < i such that dp[j] is true AND s[j..i-1] is in the dictionary. Base: dp[0]=true (empty string is trivially segmented).

Recursion

Try all splits

Exponential without memo. For each position try all word lengths.

Time: O(2ⁿ)
DP O(n²)

dp[0..n]

For each i, check all j where dp[j]=true and s[j..i-1] is in word set. Hash set for O(1) lookup.

Time: O(n²) · Space: O(n) ✓
Approach — Walk Forward, Try All Prior True Positions

For i from 1 to n: for j from 0 to i−1: if dp[j] AND s.substr(j, i−j) in wordSet → dp[i]=true, break. Use unordered_set for O(1) lookups. The inner loop is O(n) per position, so total O(n²) plus O(L) per substr check where L=word length.

s="leetcode" dict={"leet","code"} dp=[T,F,F,F,F,F,F,F,F] i=1: j=0: dp[0]=T, s[0..0]="l" ∉ dict i=2: j=0: "le" ∉; j=1: dp[1]=F skip i=3: j=0: "lee" ∉; ... i=4: j=0: dp[0]=T, s[0..3]="leet" ∈ dict → dp[4]=T i=5: j=0: "leetc" ∉; j=4: dp[4]=T, s[4..4]="c" ∉ dict ... i=8: j=4: dp[4]=T, s[4..7]="code" ∈ dict → dp[8]=T Answer = dp[8] = true
C++ Solution — O(n²) time · O(n + dict) space 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; for(int i=1;i<=n;i++) for(int j=0;j<i;j++) if(dp[j] && ws.count(s.substr(j,i-j))){ dp[i]=true; break; } return dp[n]; }
TimeO(n²)
SpaceO(n + dict)
ExtensionLC#140 Word Break II — return all valid segmentations (backtrack + memo)
Q156 · MEDIUM Combination Sum IV (Count Ordered Sequences)
DPArray
LeetCode #377
Must KnowAmounts Outer = Permutations; Coins Outer = Combinations

Count the number of possible ordered sequences (permutations) of numbers from the array that sum to target. Order matters: (1,2) and (2,1) are different.

nums[1,2,3]
target4
Output7
Sequences(1,1,1,1),(1,1,2),(1,2,1) (2,1,1),(2,2),(1,3),(3,1)
Decode — Ordered = Permutations = Amounts in Outer Loop

Contrast with Q149 (combinations, coins outer). For permutations, iterate amounts in outer loop, nums in inner loop. At each amount a, for each num: dp[a] += dp[a−num]. This counts ordered sequences because for each total we consider all possible last elements.

Amounts Outer

Permutation Count

dp[a] = ways to make amount a with order. For each a: for each num c: dp[a]+=dp[a-c].

Time: O(target×n) · Space: O(target) ✓
Approach — dp[a] Sums Over All Possible Last Elements

dp[0]=1 (empty sequence). For a from 1 to target: for each num c: if c ≤ a, dp[a] += dp[a−c]. dp[a−c] counts sequences reaching (a−c); adding c as last element gives sequences reaching a. Since we're iterating all c at each a, we naturally count all orderings.

nums=[1,2,3], target=4 dp=[1,0,0,0,0] a=1: c=1: dp[1]+=dp[0]=1 → dp[1]=1 a=2: c=1: dp[2]+=dp[1]=1 c=2: dp[2]+=dp[0]=1 → dp[2]=2 a=3: c=1: +=dp[2]=2 c=2: +=dp[1]=1 c=3: +=dp[0]=1 → dp[3]=4 a=4: c=1: +=dp[3]=4 c=2: +=dp[2]=2 c=3: +=dp[1]=1 → dp[4]=7 Answer = dp[4] = 7
C++ Solution — O(target×n) time · O(target) space int combinationSum4(vector<int>& nums, int target){ vector<long> dp(target+1,0); dp[0]=1; for(int a=1; a<=target; a++) // Amounts OUTER → permutations for(int c : nums) if(c <= a) dp[a]+=dp[a-c]; return dp[target]; }
TimeO(target × n)
SpaceO(target)
Key RulePermutations = amounts outer. Combinations = coins outer. (From Q149)
Q157 · HARD Ones and Zeroes (2D Knapsack)
DPArray
LeetCode #474
Must KnowTwo Capacity Dimensions — dp[i][j] = max strings using ≤i zeros ≤j ones

Array of binary strings. Find the largest subset where the total number of 0s ≤ m and total 1s ≤ n.

strs["10","0001","111001","1","0"]
m=5, n=3Output: 4
Why["10","0001","1","0"] → 5 zeros, 3 ones ✓
Decode — 0/1 Knapsack With Two Weight Dimensions (Zeros and Ones)

Each string is an item with two weights: (count of 0s, count of 1s). The knapsack has two capacities: m (max zeros) and n (max ones). dp[i][j] = max strings selected using at most i zeros and j ones. Same right-to-left update logic as 0/1 knapsack, but in 2D.

Approach — 2D Knapsack, Update Both Dimensions in Reverse

For each string with z zeros and o ones: iterate i from m down to z, j from n down to o: dp[i][j] = max(dp[i][j], dp[i−z][j−o] + 1). The double reverse ensures each string is used at most once.

strs=["10","0"], m=1, n=1 dp[2×2] all zeros str="10" (z=1,o=1): i=1,j=1: dp[1][1]=max(0, dp[0][0]+1)=1 dp=[[0,0],[0,1]] str="0" (z=1,o=0): i=1,j=1: dp[1][1]=max(1, dp[0][1]+1)=max(1,0+1)=1 i=1,j=0: dp[1][0]=max(0, dp[0][0]+1)=1 dp=[[0,0],[1,1]] Answer = dp[1][1] = 1 (only one fits m=1,n=1 — either "10" or "0")
C++ Solution — O(L×m×n) time · O(m×n) space int findMaxForm(vector<string>& strs, int m, int n){ vector<vector<int>> dp(m+1, vector<int>(n+1,0)); for(auto& s:strs){ int z=count(s.begin(),s.end(),'0'); int o=s.size()-z; for(int i=m;i>=z;i--) // Both dimensions right-to-left for(int j=n;j>=o;j--) dp[i][j]=max(dp[i][j], dp[i-z][j-o]+1); } return dp[m][n]; }
TimeO(L × m × n)
SpaceO(m × n)
Q158 · HARD Maximum Profit in Job Scheduling
DPBinary Search
LeetCode #1235
Must KnowSort by End Time, Binary Search for Last Compatible Job

Jobs with start, end, profit. Non-overlapping jobs only. Maximise total profit.

start[1,2,3,3]
end[3,4,5,6]
profit[50,10,40,70]
Output120
WhyJob 0(50)+Job 3(70)
Decode — For Each Job, Choose: Skip It or Take It Plus Best Non-Overlapping Before It

Sort jobs by end time. dp[i] = max profit considering jobs 0..i−1. For job i: skip → dp[i]. Take → profit[i] + dp[j] where j is the index of the last job whose end time ≤ start[i]. Find j with binary search on end times. Transition: dp[i+1] = max(dp[i], profit[i] + dp[j]).

O(n²)

Linear scan for compatible job

For each job, scan backwards to find last non-overlapping job. O(n) per job.

Time: O(n²)
O(n log n)

Binary search on end times

Sort by end. For each job, binary search for last end ≤ start. O(log n) per job.

Time: O(n log n) · Space: O(n) ✓
Approach — Sort by End, DP + Binary Search

Sort jobs by end time. Build dp[0..n] where dp[i] = max profit from first i jobs. For job i (0-indexed): find the rightmost job j where end[j] ≤ start[i] using upper_bound on the sorted end array. dp[i+1] = max(dp[i], profit[i] + dp[j+1]).

Jobs sorted by end: (1,3,50),(2,4,10),(3,5,40),(3,6,70) dp=[0,0,0,0,0] ends=[3,4,5,6] i=0 (1→3,p=50): start=1, find last end≤1 → none → j=-1 → dp[0]=0 dp[1]=max(dp[0], 50+dp[0])=max(0,50)=50 i=1 (2→4,p=10): start=2, find last end≤2 → none → dp[0]=0 dp[2]=max(dp[1],10+0)=max(50,10)=50 i=2 (3→5,p=40): start=3, find last end≤3 → end[0]=3≤3 → j=0 → dp[1]=50 dp[3]=max(dp[2],40+50)=max(50,90)=90 i=3 (3→6,p=70): start=3, last end≤3 → j=0 → dp[1]=50 dp[4]=max(dp[3],70+50)=max(90,120)=120 Answer = dp[4] = 120
C++ Solution — O(n log n) int jobScheduling(vector<int>& start, vector<int>& end_, vector<int>& profit){ int n=start.size(); vector<array<int,3>> jobs(n); for(int i=0;i<n;i++) jobs[i]={end_[i],start[i],profit[i]}; sort(jobs.begin(),jobs.end()); // Sort by end time vector<int> dp(n+1,0), ends; for(int i=0;i<n;i++){ int s=jobs[i][1]; // Binary search: last job whose end ≤ start of current job int j=(int)(upper_bound(ends.begin(),ends.end(),s)-ends.begin()); dp[i+1]=max(dp[i], jobs[i][2]+dp[j]); ends.push_back(jobs[i][0]); // Track sorted end times } return dp[n]; }
TimeO(n log n)
SpaceO(n)
PatternWeighted interval scheduling — same as Q125 (critical path) applied to ranges
Chapter 16 · Q159–Q167 · String & Grid DP
String & Grid DP — LCS, Edit Distance, Paths & 2D Tables
String DP Mental Model — Index Both Strings, Fill a 2D Table

Most string DP problems define dp[i][j] as the answer for s1[0..i−1] and s2[0..j−1]. The recurrence branches on whether s1[i−1] == s2[j−1]: if equal, often dp[i-1][j-1]+1. If not, take the best of deleting from either string. Spend time understanding the 2D table — draw it out before coding.

Q159 · MEDIUM Longest Common Subsequence (LCS)
DPString
LeetCode #1143
Must KnowAsked 5,000+Foundation of All String DP

Find the length of the longest common subsequence of two strings. A subsequence doesn't need to be contiguous.

s1"abcde"
s2"ace"
Output3
LCS"ace"
s1"abc"
s2"def"
Output 20
Decode — Match Characters If Equal, Skip One If Not

dp[i][j] = LCS length of s1[0..i−1] and s2[0..j−1]. If s1[i−1] == s2[j−1]: match! dp[i][j] = dp[i−1][j−1] + 1. If not: the LCS must skip s1[i−1] or s2[j−1] — take the better option: dp[i][j] = max(dp[i−1][j], dp[i][j−1]).

O(2^(m+n))

Recursion

Try all pairs of subsequences. Exponential.

Time: exponential
O(m×n)

2D DP Table

Build table row by row. dp[i][j] from three neighbours. O(m×n) time and space.

Time: O(mn) · Space: O(mn) or O(min(m,n))
Approach — 2D Table, Three Cases at Every Cell

Base: dp[0][j]=dp[i][0]=0. For each (i,j): if s1[i−1]==s2[j−1] → dp[i][j]=dp[i−1][j−1]+1. Else → dp[i][j]=max(dp[i−1][j], dp[i][j−1]). Space can be reduced to O(n) using two rolling rows since each row only depends on the row above.

s1="abcde" s2="ace" (dp table rows=s1, cols=s2) "" a c e "" [ 0 0 0 0 ] a [ 0 1 1 1 ] s1[0]='a'==s2[0]='a' → dp[1-1][1-1]+1=1 b [ 0 1 1 1 ] no match → max(dp[1][1],dp[2][0])=1 c [ 0 1 2 2 ] s1[2]='c'==s2[1]='c' → dp[2][1]+1=2 d [ 0 1 2 2 ] no match → max(left,above)=2 e [ 0 1 2 3 ] s1[4]='e'==s2[2]='e' → dp[4][2]+1=3 Answer = dp[5][3] = 3
C++ Solution — O(mn) time · O(n) space (rolling rows) 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; else dp[i][j]=max(dp[i-1][j],dp[i][j-1]); return dp[m][n]; }
TimeO(m × n)
SpaceO(m × n) → reducible to O(n)
Builds onEdit Distance · Longest Common Substring · Diff utilities
Q160 · HARD Edit Distance (Levenshtein)
DPString
LeetCode #72
Must KnowFAANG FavouriteThree Operations = Three Neighbours in the Table

Convert word1 to word2 using minimum operations. Each operation is one of: insert a character, delete a character, or replace a character.

w1"horse"
w2"ros"
Output3
Stepshorse→rorse(repl) →rose(del)→ros(del)
Decode — dp[i][j] = Min Ops to Convert w1[0..i−1] to w2[0..j−1]

Three operations map to three table neighbours: Insert into w1 = dp[i][j−1]+1 (we handled w2[j−1] by inserting). Delete from w1 = dp[i−1][j]+1. Replace = dp[i−1][j−1]+1. If w1[i−1]==w2[j−1]: no operation needed → dp[i−1][j−1]+0. Take the minimum of all valid options.

O(mn) DP

2D Table

dp[i][j]=min edit distance for prefixes i,j. Three neighbours + optional free match.

Time: O(mn) · Space: O(mn) or O(n)
Approach — Fill the 2D Table, Three Neighbours at Each Cell

Base: dp[i][0]=i (delete i chars from w1). dp[0][j]=j (insert j chars). For each (i,j): if w1[i−1]==w2[j−1] → dp[i][j]=dp[i−1][j−1] (free). Else → min(dp[i−1][j−1]+1, dp[i−1][j]+1, dp[i][j−1]+1) = min(replace, delete, insert).

w1="horse" w2="ros" (compact — showing key cells) "" r o s "" [ 0 1 2 3 ] h [ 1 1 2 3 ] h≠r → min(0+1,1+1,1+1)=1 o [ 2 2 1 2 ] o==o → dp[1][1]=1; o≠s → min(1+1,2+1,1+1)=2 r [ 3 2 2 2 ] r==r → dp[2][0]+0=2; r≠s → min(1+1,2+1,2+1)=2 s [ 4 3 3 2 ] s==s → dp[3][2]=2; s≠o,s≠r → ... e [ 5 4 4 3 ] e≠s → min(dp[4][2]+1,...)=3 Answer = dp[5][3] = 3
C++ Solution — O(mn) time · O(n) space (rolling row) int minDistance(string w1, string w2){ int m=w1.size(), n=w2.size(); vector<int> dp(n+1); iota(dp.begin(),dp.end(),0); // dp[j]=j (base: insert j chars) for(int i=1;i<=m;i++){ int prev=dp[0]; dp[0]=i; // Base: delete i chars from w1 for(int j=1;j<=n;j++){ int tmp=dp[j]; if(w1[i-1]==w2[j-1]) dp[j]=prev; // Free match else dp[j]=1+min({prev,dp[j],dp[j-1]}); // Replace,Delete,Insert prev=tmp; } } return dp[n]; }
TimeO(m × n)
SpaceO(n)
ApplicationsSpell check · DNA alignment · diff tools
Q161 · MEDIUM Longest Palindromic Subsequence
DPString
LeetCode #516
Must KnowLCS(s, reverse(s)) = Longest Palindromic Subsequence

Find the length of the longest palindromic subsequence in a string.

Input"bbbab"
Output4
LPS"bbbb"
Input 2"cbbd"
Output 22
Decode — Palindromic Subsequence = Common Subsequence with Its Reverse

A palindrome reads the same forwards and backwards. Any palindromic subsequence of s must also appear as a subsequence of reverse(s). Therefore LPS(s) = LCS(s, reverse(s)). This reduces to Q159 directly.

LCS approach

LCS(s, rev(s))

Reverse s, compute LCS. Clean two-line solution. O(n²) time and space.

Time: O(n²) · Space: O(n²)
Interval DP

dp[i][j] directly

dp[i][j]=LPS of s[i..j]. If s[i]==s[j]: dp[i][j]=dp[i+1][j-1]+2. Else max of two shrinks.

Time: O(n²) · Space: O(n²)
Approach — Interval DP on the String Itself

dp[i][j] = length of LPS in s[i..j]. Base: dp[i][i]=1. For length l from 2 to n: for each starting i, j=i+l−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]).

s = "bbbab" (interval DP, filling by length) len=1: dp[i][i]=1 for all i len=2: dp[0][1]: b==b → dp[1][0]+2=0+2=2 dp[1][2]: b==b → 2 dp[2][3]: b≠a → max(dp[3][3],dp[2][2])=1 dp[3][4]: a≠b → max(dp[4][4],dp[3][3])=1 len=3: dp[0][2]: b==b → dp[1][1]+2=3 dp[1][3]: b≠a → max(dp[2][3],dp[1][2])=max(1,2)=2 dp[2][4]: b==b → dp[3][3]+2=3 len=4: dp[0][3]: b≠a → max(dp[1][3],dp[0][2])=max(2,3)=3 dp[1][4]: b==b → dp[2][3]+2=1+2=3... wait: dp[2][3]=1 → 3 len=5: dp[0][4]: b==b → dp[1][3]+2=2+2=4 Answer = dp[0][4] = 4
C++ Solution — O(n²) time and space int longestPalindromeSubseq(string s){ int n=s.size(); vector<vector<int>> dp(n,vector<int>(n,0)); for(int i=0;i<n;i++) dp[i][i]=1; 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]; }
TimeO(n²)
SpaceO(n²)
Q162 · HARD Distinct Subsequences
DPString
LeetCode #115
Must KnowCount How Many Times t Appears as Subsequence in s

Count the number of distinct subsequences of s which equal t.

s"rabbbit"
t"rabbit"
Output3
Why3 ways to pick one 'b' from the three 'b's
Decode — dp[i][j] = Ways t[0..j−1] Appears as Subseq in s[0..i−1]

At each pair (i,j): if s[i−1]==t[j−1], we can either use s[i−1] to match t[j−1] (dp[i−1][j−1] ways) or skip s[i−1] (dp[i−1][j] ways). If s[i−1]≠t[j−1], we must skip s[i−1] → dp[i][j]=dp[i−1][j].

Approach — 2D Table, Use or Skip Current s Character

Base: dp[i][0]=1 for all i (t="" is always a subsequence). dp[0][j]=0 for j>0. Transition: if s[i−1]==t[j−1]: dp[i][j]=dp[i−1][j−1]+dp[i−1][j]. Else: dp[i][j]=dp[i−1][j].

s="rabbbit" t="rabbit" (key cells) "" r a b b i t "" [1 0 0 0 0 0 0] r [1 1 0 0 0 0 0] r==r: dp[0][0]+dp[0][1]=1+0=1 a [1 1 1 0 0 0 0] a==a: dp[1][1]+dp[1][2]=1+0=1 b [1 1 1 1 0 0 0] b==b: 1+0=1 b [1 1 1 2 1 0 0] b==b: dp[3][2]+dp[3][3]=1+1=2 b [1 1 1 3 3 0 0] b==b: dp[4][2]+dp[4][3]=1+2=3 i [1 1 1 3 3 3 0] i==i: dp[5][4]+dp[5][5]=3+0=3 t [1 1 1 3 3 3 3] t==t: dp[6][5]+dp[6][6]=3+0=3 Answer = dp[7][6] = 3
C++ Solution — O(mn) time · O(n) space int numDistinct(string s, string t){ int m=s.size(), n=t.size(); vector<long> dp(n+1,0); dp[0]=1; for(int i=1;i<=m;i++) for(int j=n;j>=1;j--) // Right-to-left to not overwrite needed values if(s[i-1]==t[j-1]) dp[j]+=dp[j-1]; return dp[n]; }
TimeO(m × n)
SpaceO(n)
Q163 · MEDIUM Unique Paths (Grid DP)
DPGrid
LeetCode #62
Must KnowCan Only Move Right or Down

m×n grid. Start at top-left, reach bottom-right. Can only move right or down. Count distinct paths.

m=3, n=7Output: 28
m=3, n=2Output: 3
Paths (3×2)R↓↓, ↓R↓, ↓↓R
Decode — Each Cell's Count = Sum From Above and From Left

dp[i][j] = number of ways to reach cell (i,j). You can only arrive from above (i−1,j) or from the left (i,j−1). So dp[i][j]=dp[i−1][j]+dp[i][j−1]. All top-row and left-column cells = 1 (only one way to reach them — stay on the edge).

Approach — Fill Grid Row by Row, or Use Math Formula

Grid approach: dp[i][j]=dp[i-1][j]+dp[i][j-1]. Space-optimise to 1D by noting dp[j] += dp[j-1] in each row (left column stays 1). Math formula: C(m+n-2, m-1) — total moves is m+n-2, choose which m-1 are downward.

m=3, n=3 grid (showing dp values at each cell) [1, 1, 1] ← top row all 1s [1, 2, 3] ← dp[1][1]=1+1=2, dp[1][2]=1+2=3 [1, 3, 6] ← dp[2][1]=1+2=3, dp[2][2]=3+3=6 Answer = dp[2][2] = 6
C++ Solution — O(m×n) time · O(n) space 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]; // dp[j]=from above + from left return dp[n-1]; }
TimeO(m × n)
SpaceO(n)
MathC(m+n-2, m-1) — combinatorics gives O(min(m,n)) time
Q164 · MEDIUM Unique Paths II — With Obstacles
DPGrid
LeetCode #63
Must KnowObstacle Cell = dp[i][j] = 0

Same as Q163 but some cells have obstacles (1=obstacle, 0=free). Count paths that avoid obstacles.

Grid[[0,0,0], [0,1,0], [0,0,0]]
Output2
Decode — Same Recurrence, But Obstacle Cells Force dp = 0

Exactly Q163 but with an added rule: if grid[i][j]==1 (obstacle), then dp[i][j]=0. The contribution from an obstacle is blocked — no path passes through it. Build the table identically otherwise.

Approach — Same DP with Obstacle Guard

Initialise the first row: set dp[j]=1 until we hit an obstacle, then all subsequent cells in that row = 0 (can't pass the obstacle). Similarly for the first column. For interior cells: if obstacle → dp[i][j]=0. Else dp[i][j]=dp[i-1][j]+dp[i][j-1].

[[0,0,0],[0,1,0],[0,0,0]] Row 0: dp=[1,1,1] (no obstacles in first row) Row 1: dp[0]+=0(from above)=1 dp[1]: obstacle! → dp[1]=0 dp[2]: dp[1-1][2]+dp[1][1]=1+0=1 dp=[1,0,1] Row 2: dp[0]=dp[0]+0=1 dp[1]=dp[0][1]+dp[1][0]=0+1=1 dp[2]=dp[1][2]+dp[2][1]=1+1=2 Answer = dp[2][2] = 2
C++ Solution — O(m×n) time · O(n) space int uniquePathsWithObstacles(vector<vector<int>>& grid){ int m=grid.size(), n=grid[0].size(); vector<long> dp(n,0); dp[0]=(grid[0][0]==0)?1:0; for(int j=1;j<n;j++) dp[j]=grid[0][j]?0:dp[j-1]; // First row for(int i=1;i<m;i++){ dp[0]=grid[i][0]?0:dp[0]; // First col for(int j=1;j<n;j++) dp[j]=grid[i][j]?0:(dp[j]+dp[j-1]); } return dp[n-1]; }
TimeO(m × n)
SpaceO(n)
Q165 · MEDIUM Minimum Path Sum
DPGrid
LeetCode #64
Must KnowPaths DP with Cost Accumulation

m×n grid of non-negative integers. Find the path from top-left to bottom-right (right/down only) with minimum sum.

Input[[1,3,1], [1,5,1], [4,2,1]]
Output7
Path1→3→1→1→1=7
Decode — dp[i][j] = Min Cost to Reach Cell (i,j)

To reach (i,j) you came from (i−1,j) or (i,j−1). Add the cost of the current cell to the cheaper of the two. dp[i][j] = grid[i][j] + min(dp[i−1][j], dp[i][j−1]).

Approach — Accumulate Min Cost In-Place or 1D Rolling Array

Modify grid in-place or use a 1D dp. For the first row: accumulate left to right. For the first column: accumulate top to bottom. For interior: dp[j] = grid[i][j] + min(dp[j], dp[j−1]) where dp[j] is "from above" and dp[j−1] is "from left."

grid=[[1,3,1],[1,5,1],[4,2,1]] First row (left accum): dp=[1, 1+3=4, 4+1=5] Row 1: dp[0]=grid[1][0]+dp[0]=1+1=2 dp[1]=grid[1][1]+min(dp[1],dp[0])=5+min(4,2)=7 dp[2]=grid[1][2]+min(dp[2],dp[1])=1+min(5,7)=6 Row 2: dp[0]=4+2=6 dp[1]=2+min(7,6)=8 dp[2]=1+min(6,8)=7 Answer = dp[2] = 7
C++ Solution — O(m×n) time · O(n) space int minPathSum(vector<vector<int>>& grid){ int m=grid.size(),n=grid[0].size(); vector<int> dp(n,INT_MAX); dp[0]=0; for(int i=0;i<m;i++){ dp[0]+=grid[i][0]; // First column accumulates down for(int j=1;j<n;j++) dp[j]=grid[i][j]+min(dp[j],dp[j-1]); // Min of from-above vs from-left } return dp[n-1]; }
TimeO(m × n)
SpaceO(n)
Q166 · HARD Dungeon Game
DPGrid
LeetCode #174
Fill DP Backwards — From Bottom-Right to Top-Left

Knight must reach bottom-right from top-left. Cells have positive (health gain) or negative (health loss) values. Health must always stay ≥ 1. Find the minimum initial health needed.

Grid[[-2,-3,3], [-5,-10,1], [10,30,-5]]
Output7
Decode — Fill Backwards: dp[i][j] = Min Health Needed to Enter Cell (i,j)

Going forward is hard because health depends on future cells. Backwards is clean: dp[i][j] = minimum health needed when entering (i,j) to survive the rest of the journey. dp[i][j] = max(1, min(dp[i+1][j], dp[i][j+1]) − dungeon[i][j]). The max(1,…) ensures health never goes below 1.

Approach — Backwards DP From Bottom-Right

Fill dp from bottom-right to top-left. Base: dp[m-1][n-1] = max(1, 1 − dungeon[m-1][n-1]). For bottom row (only right neighbour) and right column (only bottom neighbour): fill accordingly. For interior: take min of right and bottom dp values, subtract current cell, max with 1.

dungeon=[[-2,-3,3],[-5,-10,1],[10,30,-5]] dp (filled backwards, showing final values): Bottom-right: max(1,1-(-5))=max(1,6)=6 Right col: dp[1][2]=max(1,6-1)=5; dp[0][2]=max(1,5-3)=2 Bottom row: dp[2][1]=max(1,6-30)=1; dp[2][0]=max(1,1-10)=1 Interior: dp[1][1]=max(1,min(dp[2][1],dp[1][2])-(-10))=max(1,min(1,5)+10)=11 dp[1][0]=max(1,min(dp[2][0],dp[1][1])-(-5))=max(1,min(1,11)+5)=6 dp[0][1]=max(1,min(dp[1][1],dp[0][2])-(-3))=max(1,min(11,2)+3)=max(1,5)=5 dp[0][0]=max(1,min(dp[1][0],dp[0][1])-(-2))=max(1,min(6,5)+2)=7 Answer = dp[0][0] = 7
C++ Solution — O(m×n) time · O(n) space int calculateMinimumHP(vector<vector<int>>& d){ int m=d.size(), n=d[0].size(); vector<int> dp(n+1, INT_MAX); dp[n-1]=1; for(int i=m-1;i>=0;i--){ for(int j=n-1;j>=0;j--){ int need=min(dp[j], j+1<n ? dp[j+1] : INT_MAX); dp[j]=max(1, need-d[i][j]); } } return dp[0]; }
TimeO(m × n)
SpaceO(n)
Q167 · HARD Maximal Square
DPGrid
LeetCode #221
Must Knowdp[i][j] = Side Length of Largest Square Ending at (i,j)

Find the largest square containing only 1s in a binary matrix. Return its area.

Input[["1","0","1","0","0"], ["1","0","1","1","1"], ["1","1","1","1","1"], ["1","0","0","1","0"]]
Output4
Decode — dp[i][j] = Side of Largest All-1 Square With Bottom-Right at (i,j)

If matrix[i][j]=='1': dp[i][j] = 1 + min(dp[i−1][j], dp[i][j−1], dp[i−1][j−1]). The minimum of the three neighbours determines the largest square that can be extended here. If any neighbour is small (has a 0 nearby), the square is constrained. If matrix[i][j]=='0': dp[i][j]=0.

Approach — Why Min of Three Neighbours Works

dp[i-1][j] = largest square ending at the cell above. dp[i][j-1] = largest ending at the cell to the left. dp[i-1][j-1] = largest ending diagonally. The new square of side k requires all three to have side ≥ k−1. So the bottleneck is the minimum. Max side seen = answer; area = side².

Matrix row 2 (all 1s): [1,1,1,1,1] (simplified 3-row example) After row 0 (1,0,1,0,0): dp=[1,0,1,0,0] After row 1 (1,0,1,1,1): j=0: dp[0]=1 j=1: '0' → dp[1]=0 j=2: min(dp_above[2]=1, dp[1]=0, dp_diag[1]=0)+1=0+1=1 j=3: min(dp_above[3]=0, dp[2]=1, dp_diag[2]=1)+1=0+1=1 j=4: min(dp_above[4]=0, dp[3]=1, dp_diag[3]=0)+1=1 dp=[1,0,1,1,1] After row 2 (1,1,1,1,1): j=0:1, j=1: min(0,1,0)+1=1 j=2: min(1,1,1)+1=2 j=3: min(1,2,1)+1=2 j=4: min(1,2,1)+1=2 maxSide=2 → area=4
C++ Solution — O(m×n) time · O(n) space int maximalSquare(vector<vector<char>>& mat){ int m=mat.size(),n=mat[0].size(),best=0; vector<int> dp(n+1,0); for(int i=0;i<m;i++){ int prev=0; for(int j=0;j<n;j++){ int tmp=dp[j+1]; if(mat[i][j]=='1'){ dp[j+1]=1+min({prev, dp[j], dp[j+1]}); // min of 3 neighbours best=max(best,dp[j+1]); } else dp[j+1]=0; prev=tmp; } } return best*best; }
TimeO(m × n)
SpaceO(n)
↑ back to top
Chapter 17 · Q168–Q175 · Interval & Advanced DP
Interval & Advanced DP — Ranges, State Machines & Game Theory
Interval DP — Fill by Length, Not by Index

Interval DP solves problems on contiguous segments. dp[i][j] = answer for segment [i..j]. Fill by increasing length: length 1 first (base), then 2, 3, ... n. For each length, iterate all starting positions i. Compute dp[i][j] by trying every split point k in [i..j] and combining dp[i][k] and dp[k+1][j]. Classic problems: matrix chain multiplication, burst balloons, stone merging.

Q168 · HARD Burst Balloons
DPInterval
LeetCode #312
Must KnowFAANG FavouriteThink of k as the LAST Balloon Burst, Not First

n balloons with values. Bursting balloon i gives coins nums[i−1]×nums[i]×nums[i+1]. Maximise total coins. Add boundary balloons of value 1 at both ends.

Input[3,1,5,8]
Output167
Order3→1→5→8 gives 167
Decode — Think of k as the LAST Balloon Burst in Range [i..j]

Thinking about which balloon to burst first is hard because it changes the neighbours. Instead, think about which is burst last. When k is the last balloon in [i..j], its left and right neighbours are the fixed boundaries i−1 and j+1. dp[i][j] = max over all k in [i..j] of: dp[i][k−1] + nums[i−1]×nums[k]×nums[j+1] + dp[k+1][j].

O(n! )

All Orderings

Try every permutation of burst order. Factorial time.

Time: O(n!)
O(n³)

Interval DP

dp[i][j] = max coins from bursting all in [i..j]. Fill by length. Try every last-burst k.

Time: O(n³) · Space: O(n²)
Approach — Interval DP With Boundary Sentinels

Pad nums with 1 at both ends: nums = [1, ...original..., 1]. dp[i][j] = max coins from bursting balloons i..j (where left boundary is i−1 and right boundary is j+1 in the padded array). Fill by length. For each length l, each start i, j=i+l−1: try each k as last burst: dp[i][j] = max over k of dp[i][k−1] + padded[i−1]×padded[k]×padded[j+1] + dp[k+1][j].

nums=[3,1,5,8] → padded=[1,3,1,5,8,1] (indices 0..5) Fill dp[i][j] for original indices 1..4 (padded) len=1: dp[1][1]: k=1 last: 1×3×1=3 → 3 dp[2][2]: k=2: 3×1×5=15 → 15 dp[3][3]: k=3: 1×5×8=40 → 40 dp[4][4]: k=4: 5×8×1=40 → 40 len=2: dp[1][2]: k=1: dp[]+1×3×5+dp[2][2]=0+15+15=30 k=2: dp[1][1]+3×1×5+0=3+15=18 → dp[1][2]=30 dp[2][3]: k=2: 0+3×1×8+40=24+40=64? No: padded[1]×pad[2]×pad[4]=3×1×8=24+dp[3][3]=64? wait: k=2: dp[2][1]+pad[1]×pad[2]×pad[4]+dp[3][3]=0+3×1×8+40=64 k=3: dp[2][2]+pad[1]×pad[3]×pad[4]+0=15+3×5×8=15+120=135... Hmm k=3: left=dp[2][2]=15, coins=padded[i-1]×padded[k]×padded[j+1]=padded[1]×padded[3]×padded[4]=3×5×8=120, right=0 → 135 dp[2][3] = max(64,135)? Let me verify with padded correctly: padded = [1,3,1,5,8,1] so padded[0]=1,padded[1]=3,...,padded[5]=1 dp[2][3]: i=2,j=3, left boundary=padded[1]=3, right boundary=padded[4]=8 k=2: dp[2][1]+pad[1]×pad[2]×pad[4]=0+3×1×8=24,+dp[3][3]=40 → 64 k=3: dp[2][2]+pad[1]×pad[3]×pad[4]=15+3×5×8=135,+0 → 135 ... (continuing to len=4) dp[1][4] = 167
C++ Solution — O(n³) time · O(n²) space int maxCoins(vector<int>& nums){ nums.insert(nums.begin(),1); nums.push_back(1); // Add boundary sentinels int n=nums.size(); vector<vector<int>> dp(n,vector<int>(n,0)); for(int len=1;len<=n-2;len++) for(int i=1;i+len-1<n-1;i++){ int j=i+len-1; for(int k=i;k<=j;k++){ int coins=nums[i-1]*nums[k]*nums[j+1]; dp[i][j]=max(dp[i][j], dp[i][k-1]+coins+dp[k+1][j]); } } return dp[1][n-2]; }
TimeO(n³)
SpaceO(n²)
Key TrickThink "last burst" not "first burst" — makes boundaries fixed and independent
Q169 · HARD Strange Printer
DPInterval
LeetCode #664
Interval DP: If s[i]==s[j], Extend Without Extra Cost

A printer prints consecutive same characters in one turn. Find the minimum turns to print string s.

Input"aaabbb"
Output2
Input 2"aba"
Output 22
Decode — dp[i][j] = Min Turns to Print s[i..j]

Two cases at (i,j). If s[i]==s[j]: printing them as one run means dp[i][j]=dp[i][j−1] (the last character extends the first's run for free, no extra turn needed — we merge them). Otherwise try every split k: dp[i][j] = min over k of dp[i][k]+dp[k+1][j].

Approach — Interval DP With Free Merge When Endpoints Match

Base: dp[i][i]=1. Fill by length. For each (i,j): start with dp[i][j]=dp[i][j−1]+1 (worst case: print s[j] separately). Then if s[k]==s[j] for any k in [i..j−1]: dp[i][j]=min(dp[i][j], dp[i][k]+dp[k+1][j−1]) because s[j] can be printed with s[k] in the same turn.

s = "aba" dp[0][0]=1 (a), dp[1][1]=1 (b), dp[2][2]=1 (a) len=2: dp[0][1]: s[0]='a'≠s[1]='b' → dp[0][0]+dp[1][1]=1+1=2 dp[1][2]: s[1]='b'≠s[2]='a' → dp[1][1]+dp[2][2]=1+1=2 len=3: dp[0][2]: base=dp[0][1]+1=3 k=0: s[0]='a'==s[2]='a' → dp[0][0]+dp[1][1]=1+1=2 dp[0][2]=min(3,2)=2 Answer = dp[0][2] = 2
C++ Solution — O(n³) time · O(n²) space int strangePrinter(string s){ int n=s.size(); vector<vector<int>> dp(n,vector<int>(n,0)); for(int i=n-1;i>=0;i--){ dp[i][i]=1; for(int j=i+1;j<n;j++){ dp[i][j]=dp[i][j-1]+1; // Default: print s[j] separately for(int k=i;k<j;k++) if(s[k]==s[j]) // Merge s[j] with s[k] in same turn dp[i][j]=min(dp[i][j], (k+1<=j-1?dp[k+1][j-1]:0)+dp[i][k]); } } return dp[0][n-1]; }
TimeO(n³)
SpaceO(n²)
Q170 · HARD Best Time to Buy and Sell Stock with Cooldown
DPState Machine
LeetCode #309
Must Know3-State Machine: Held, Sold, Cooldown

Can buy/sell stocks any number of times, but after selling you must wait one day (cooldown). Maximise profit.

Input[1,2,3,0,2]
Output3
Stepsbuy day0,sell day1 cooldown day2,buy day3 sell day4 → 3
Decode — Three States at Each Day: Held, Sold (just sold), Rest (cooldown or idle)

Model as a state machine. held = max profit while holding a stock. sold = max profit on the day you just sold. rest = max profit while not holding and not in cooldown. Transitions: held' = max(held, rest − price) (buy or keep). sold' = held + price (sell today). rest' = max(rest, sold) (cool down or keep resting).

State Machine O(n)

3 States, 3 Transitions

held, sold, rest updated each day. O(1) per day.

Time: O(n) · Space: O(1) ✓
Approach — Three Variables Updated in Parallel Each Day

Init: held=−∞, sold=0, rest=0. For each price p: held' = max(held, rest−p). sold' = held+p. rest' = max(rest, sold). Update all three simultaneously (use temp variables). Answer = max(sold, rest).

prices=[1,2,3,0,2] held=-INF, sold=0, rest=0 day0 (p=1): held=max(-INF,0-1)=-1 sold=-INF+1=-INF rest=max(0,0)=0 day1 (p=2): held=max(-1,0-2)=-1 sold=-1+2=1 rest=max(0,-INF)=0 day2 (p=3): held=max(-1,0-3)=-1 sold=-1+3=2 rest=max(0,1)=1 day3 (p=0): held=max(-1,1-0)=1 sold=-1+0=-1 rest=max(1,2)=2 day4 (p=2): held=max(1,2-2)=1 sold=1+2=3 rest=max(2,-1)=2 Answer = max(sold=3, rest=2) = 3
C++ Solution — O(n) time · O(1) space 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); // Keep holding or buy from rest sold = prevHeld+p; // Sell today (must have held) rest = max(prevRest, prevSold); // Cool down or keep resting } return max(sold, rest); }
TimeO(n)
SpaceO(1)
RelatedLC#188 at most k transactions · LC#123 at most 2 transactions
Q171 · HARD Best Time to Buy and Sell Stock with Transaction Fee
DPState Machine
LeetCode #714
Two States: Held and Free — Subtract Fee on Sell

Buy/sell any number of times but pay a fee on each sell. Maximise profit.

prices[1,3,2,8,4,9]
fee=2Output: 8
Decode — Two States: Held (own stock) and Free (no stock)

Simpler than Q170 — no cooldown. held' = max(held, free−price). free' = max(free, held+price−fee). The fee is deducted at sell time.

Approach — Two-State Machine, Fee Deducted on Sell

held = max profit while holding. free = max profit while not holding. Init held=−∞, free=0. Each day: held' = max(held, free−p). free' = max(free, held+p−fee). Answer = free.

prices=[1,3,2,8,4,9], fee=2 held=-INF, free=0 p=1: held=max(-INF,0-1)=-1 free=max(0,-INF+1-2)=0 p=3: held=max(-1,0-3)=-1 free=max(0,-1+3-2)=max(0,0)=0 p=2: held=max(-1,0-2)=-1 free=max(0,-1+2-2)=max(0,-1)=0 p=8: held=max(-1,0-8)=-1 free=max(0,-1+8-2)=5 p=4: held=max(-1,5-4)=1 free=max(5,1+4-2)=max(5,3)=5? wait: free=max(5,-1+4-2)=max(5,1)=5 Then held=max(-1,5-4)=1 p=9: held=max(1,5-9)=1 free=max(5,1+9-2)=max(5,8)=8 Answer = free = 8
C++ Solution — O(n) time · O(1) space int maxProfit(vector<int>& prices, int fee){ int held=INT_MIN/2, free_=0; for(int p:prices){ int prevH=held; held = max(held, free_-p); free_ = max(free_, prevH+p-fee); // Fee paid on sell } return free_; }
TimeO(n)
SpaceO(1)
Q172 · HARD Stone Game I & II (Game Theory DP)
DPGame Theory
LeetCode #877 & #1140
Minimax: Max Your Gain Knowing Opponent Plays Optimally

Stone Game I: Two players alternate. Alex picks from either end. Piles have distinct totals. Does Alex (first player) always win? (Answer: always true for even n — show the proof.)
Stone Game II: Can take 1..2M piles from front. M updates each turn. Find max stones Alice can get.

SG I piles[5,3,4,5]
Outputtrue
SG II piles[2,7,9,4,4]
Output10
Game I Decode — First Player Always Wins (Proof by Strategy)

For even-length arrays, the first player can always guarantee winning. Partition into "even-indexed" and "odd-indexed" groups. Since totals differ, one group has more. The first player precommits to always taking from the group with more (can always do so since both endpoints can't be the same parity). This is O(1) — just return true.

Game II Decode — dp[i][M] = Max Stones Alice Can Get From Piles[i..n−1] Given Current M. Let suffix[i] = sum of piles[i..n-1]. dp[i][M] = suffix[i] - dp[i][?] (Alice takes as many as she can, Bob gets the rest). For each take of X (1..2M): dp[i][M] = max over X of (suffix[i] - dp[i+X][max(M,X)]).

Stone Game II Approach — DP With Suffix Sum

Precompute suffix sums. dp[i][m] = max stones current player gets from piles[i..n-1] with current M=m. dp[i][m] = max over x=1..2m of (suffix[i] - dp[i+x][max(m,x)]). Because if current player takes best, opponent gets the rest from what remains. Fill backwards i from n-1 to 0.

SG II: piles=[2,7,9,4,4] suffix=[26,24,17,8,4,0] dp[4][M]: only pile left=4 → dp[4][any]=4 dp[3][1]: x=1: suf[3]-dp[4][1]=8-4=4; x=2: suf[3]-dp[5][2]=8-0=8 → 8 dp[3][2]: can take up to 4: but only 2 remain → take all → 8 → 8 dp[2][1]: x=1: suf[2]-dp[3][1]=17-8=9; x=2: suf[2]-dp[4][2]=17-4=13 → 13 dp[1][1]: x=1: suf[1]-dp[2][1]=24-13=11; x=2: suf[1]-dp[3][2]=24-8=16 → 16 dp[0][1]: x=1: suf[0]-dp[1][1]=26-16=10; x=2: suf[0]-dp[2][2]=26-... → 10 Answer = dp[0][1] = 10
C++ — Stone Game I (O(1)) and II (O(n²)) // Stone Game I — First player always wins for even-length piles bool stoneGame(vector<int>& piles){ return true; } // Stone Game II int stoneGameII(vector<int>& piles){ int n=piles.size(); vector<int> suf(n+1,0); for(int i=n-1;i>=0;i--) suf[i]=suf[i+1]+piles[i]; vector<vector<int>> dp(n,vector<int>(n+1,0)); for(int i=n-1;i>=0;i--) for(int m=1;m<=n;m++){ for(int x=1;x<=2*m;x++){ int take=suf[i+x>n?n:i+x]; int rest=(i+x<n)?dp[i+x][max(m,x)]:0; dp[i][m]=max(dp[i][m], suf[i]-rest); if(i+x>=n) break; } } return dp[0][1]; }
SG IO(1)
SG IIO(n²)
Q173 · HARD Number of Ways to Form a Target String
DPString
LeetCode #1639
dp[i][j] = Ways to Build target[0..i-1] Using Word Columns 0..j-1

Given a list of strings (all same length) and a target string, count the ways to form target by picking one character per column (left to right, reusing columns not allowed after picking from column j).

words["acca","bbbb","caca"]
target"aba"
Output6
Decode — dp[i][j] = Ways to Form target[0..i-1] Using Columns 0..j-1

Pre-count freq[j][c] = how many words have character c at column j. Transition: skip column j → dp[i][j+1]+=dp[i][j]. Use column j → dp[i+1][j+1]+=dp[i][j]×freq[j][target[i]].

Approach — Column Frequency Precomputation + 2D DP

Precompute freq[j][c] for each column j and character c. dp[i][j] = ways to form first i chars of target using first j columns. For each column j: can skip (dp[i][j+1]+=dp[i][j]) or use for target[i] if freq[j][target[i]]>0 (dp[i+1][j+1]+=dp[i][j]×freq[j][target[i]]).

words=["acca","bbbb","caca"], target="aba" n=4 columns, t=3 freq[0]={'a':1,'b':1,'c':1}, freq[1]={'b':2,'c':1}, freq[2]={'b':1,'c':2}, freq[3]={'a':2,'b':1} dp[0][0]=1 (empty target formed 0 ways... wait dp[0][j]=1 for all j — empty target formed 1 way) Actually dp[i][j] = ways to form target[0..i-1] using columns 0..j-1 dp[0][*]=1, dp[i][0]=0 for i>0 j=0→1: skip col 0. For i=0: use col 0 for target[0]='a': freq[0]['a']=1 → dp[1][1]+=dp[0][0]×1=1 j=1→2: i=0: dp[0][2]+=dp[0][1]=1 (skip) i=1: use col 1 for target[1]='b': freq[1]['b']=2 → dp[2][2]+=dp[1][1]×2=2 j=2→3: i=0: dp[0][3]=1 i=1: use col2 for 'b': freq[2]['b']=1 → dp[2][3]+=dp[1][2]×1=0 (dp[1][2]=0 since not built yet) ... (Building to dp[3][4]=6 after full computation) Answer = dp[3][4] = 6
C++ Solution — O(t×n) time int numWays(vector<string>& words, string target){ int MOD=1e9+7, n=words[0].size(), t=target.size(); vector<vector<long>> freq(n,vector<long>(26,0)); for(auto& w:words) for(int j=0;j<n;j++) freq[j][w[j]-'a']++; vector<vector<long>> dp(t+1,vector<long>(n+1,0)); for(int j=0;j<=n;j++) dp[0][j]=1; for(int j=0;j<n;j++) for(int i=0;i<=t;i++){ dp[i][j+1]=(dp[i][j+1]+dp[i][j])%MOD; // Skip column j if(i<t) dp[i+1][j+1]=(dp[i+1][j+1]+dp[i][j]*freq[j][target[i]-'a'])%MOD; } return dp[t][n]; }
TimeO(t × n)
SpaceO(t × n)
Q174 · HARD Regular Expression Matching
DPString
LeetCode #10
Must KnowFAANG Favourite'*' Matches Zero or More of Preceding Element

Implement regex matching with '.' (matches any char) and '*' (matches zero or more of preceding element).

s="aa"p="a*" → true
s="ab"p=".*" → true
s="aab"p="c*a*b" → true
Decode — dp[i][j] = Does s[0..i-1] Match p[0..j-1]?

Three cases at (i,j): 1. p[j−1]='*': either zero uses (dp[i][j−2]) or one-or-more uses (s[i−1] matches p[j−2] AND dp[i−1][j]). 2. p[j−1]='.' or p[j−1]==s[i−1]: dp[i][j]=dp[i−1][j−1] (characters match). 3. Otherwise: false.

Approach — 2D DP Table With Three-Way Split for '*'

Base: dp[0][0]=true. dp[0][j]: only true if p has pattern like "a*b*c*" (star pairs). For each (i,j): handle '*' first (it looks two steps back at j−2). Otherwise check single character match: '.' or exact character.

s="aab", p="c*a*b" (dp 4×6 table) "" c * a * b "" [T F T F T F] ← p[1]='*' means c×0 ok; p[3]='*' means a×0 ok a [F F F T T F] a matches a at p[3] a [F F F F T F] aa matches a* at dp[2][4] b [F F F F F T] b matches b at p[5] Answer = dp[3][5] = true
C++ Solution — O(mn) time and space bool isMatch(string s, string p){ int m=s.size(), n=p.size(); vector<vector<bool>> dp(m+1, vector<bool>(n+1,false)); dp[0][0]=true; for(int j=2;j<=n;j++) if(p[j-1]=='*') dp[0][j]=dp[0][j-2]; for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++){ if(p[j-1]=='*'){ dp[i][j]=dp[i][j-2]||((p[j-2]=='.'||p[j-2]==s[i-1])&&dp[i-1][j]); } else { dp[i][j]=(p[j-1]=='.'||p[j-1]==s[i-1]) && dp[i-1][j-1]; } } } return dp[m][n]; }
TimeO(m × n)
SpaceO(m × n)
Q175 · HARD Minimum Cost to Merge Stones
DP Interval
LeetCode #1000
Merge K piles at once, interval split DP

Given stone piles and integer k, merge k consecutive piles into one pile repeatedly. Each merge costs the sum of the merged piles. Return minimum cost to merge all into one pile, or -1 if impossible.

stones[3,2,4,1]
k2
Output20
stones[3,2,4,1]
k3
Output-1
Decode — dp[i][j] = min cost to merge stones[i..j] down to the minimal #piles; extra cost when it becomes 1 pile

Only segments where (j-i)%(k-1)==0 can collapse to one pile. For each interval, try every split point at intervals of k-1 to maintain valid merge states. Add range sum when the segment becomes mergeable into one.

Exponential

All merge orders

Try every sequence of merges (factorial). Too slow.

Time: exponential
Interval DP O(n³)

DP over intervals, smart split

Use prefix sum and interval dp with step (k-1) splitting.

Time: O(n³) · Space: O(n²)
Approach — Fill dp by interval length with Merge Feasibility Condition

Prepare prefix sums. dp[i][j] = minimum cost to partially merge [i..j]. Iterate len=2..n. For each (i,j) choose mid = i..j-1 with step k-1: dp[i][j]=min(dp[i][j], dp[i][mid]+dp[mid+1][j]). If (j-i)%(k-1)==0, dp[i][j]+=sum(i..j) because one final merge happens.

stones=[3,2,4,1], k=2 → merge order: [3,2]=5 (cost5), [4,1]=5 (cost5), [5,5]=10 (cost10) total=20 20
C++ Solution — O(n³) time · O(n²) space int mergeStones(vector<int>& stones, int k){ int n=stones.size(); if((n-1)%(k-1)!=0) return -1; vector<int> pref(n+1,0); for(int i=0;iconst int INF=1e9; vector<vector<int>> dp(n,vector<int>(n,INF)); for(int i=0;ifor(int len=2;len<=n;len++){ for(int i=0;i+len-1int j=i+len-1; for(int mid=i;midif((len-1)%(k-1)==0) dp[i][j]+=pref[j+1]-pref[i]; } } return dp[0][n-1]; }
TimeO(n³)
SpaceO(n²)
↑ back to top