Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
What is Dynamic Programming?
Dynamic Programming solves complex problems by breaking them into overlapping subproblems and storing results to avoid redundant computation. It transforms exponential solutions into polynomial ones. Think of computing Fibonacci numbers naively.fib(5) calls fib(4) and fib(3). fib(4) calls fib(3) and fib(2). Notice that fib(3) is computed twice — and the redundancy explodes exponentially. DP eliminates this by storing each result the first time it is computed and reusing it for free afterward. The Fibonacci tree collapses from O(2^n) calls into O(n) lookups.
Two conditions must hold for DP to apply:
- Optimal substructure: The optimal solution to the problem contains optimal solutions to its subproblems. (The shortest path from A to C through B uses the shortest path from A to B.)
- Overlapping subproblems: The same subproblems are solved repeatedly. (If subproblems are independent and non-overlapping, you have divide and conquer instead.)
Pattern Recognition Checklist
Use DP When
- Problem has optimal substructure
- Same subproblems solved repeatedly
- Need to find min/max/count
- Choices at each step affect result
- Can break into smaller similar problems
Don't Use When
- Greedy choice works (no need to try all)
- No overlapping subproblems
- Need all solutions (use Backtracking)
- Simple iteration is sufficient
When to Use
Optimal Substructure
Overlapping Subproblems
Counting Problems
Optimization
The IDEAL Framework
Pattern Variations
1. 1D DP (Linear)
The simplest DP pattern. The state is a single index (usually position or amount), and each state depends on a constant number of previous states. Climbing Stairs: At stepi, you could have arrived from step i-1 (one step) or i-2 (two steps). So dp[i] = dp[i-1] + dp[i-2] — this is Fibonacci with different base cases.
Space optimization insight: Since dp[i] only depends on dp[i-1] and dp[i-2], you do not need the full array — just two variables. This reduces space from O(n) to O(1). Always look for this pattern in 1D DP.
Time: O(n). Space: O(n) or O(1) optimized.
2. 2D DP (Grid/Sequence)
When the state depends on two dimensions (e.g., position in a grid, or two indices into two strings), you need a 2D DP table. Unique Paths: Count how many ways a robot can go from the top-left corner to the bottom-right corner of anm x n grid, moving only right or down. Each cell (i, j) can only be reached from (i-1, j) (above) or (i, j-1) (left), so dp[i][j] = dp[i-1][j] + dp[i][j-1]. The first row and first column are all 1s because there is only one way to reach them (straight line).
Walked example: 3x3 grid. dp[0][*] = [1,1,1], dp[*][0] = [1,1,1]. dp[1][1] = dp[0][1] + dp[1][0] = 1+1 = 2. dp[1][2] = dp[0][2] + dp[1][1] = 1+2 = 3. dp[2][1] = dp[1][1] + dp[2][0] = 2+1 = 3. dp[2][2] = dp[1][2] + dp[2][1] = 3+3 = 6. Answer: 6 paths.
Space optimization insight: Row i only depends on row i-1. So we can use a 1D array of size n and update it in place, left to right. dp[j] += dp[j-1] effectively computes old_dp[j] + dp[j-1] because dp[j] still holds the value from the previous row when we read it.
Time: O(m * n). Space: O(m * n) or O(n) optimized.
3. Knapsack Pattern
The 0/1 knapsack is the gateway to a huge family of DP problems. At each item, you make a binary choice: take it or skip it. This “take or skip” decision structure appears in Partition Equal Subset Sum, Target Sum, Coin Change (bounded), and many more. State:dp[i][w] = maximum value achievable using the first i items with capacity w.
Transition: For each item, either skip it (dp[i-1][w]) or take it (dp[i-1][w - weight_i] + value_i). Take the max.
Space optimization: Since row i only depends on row i-1, use a 1D array. The trick: iterate w backwards so that dp[w - weight_i] still reflects the previous row (not the current one). Iterating forwards accidentally allows item reuse (turning 0/1 into unbounded knapsack).
Time: O(n * W). Space: O(n * W) or O(W) optimized.
4. LCS Pattern (Two Strings)
The Longest Common Subsequence (LCS) is the foundation for a huge family of two-string DP problems: edit distance, shortest common supersequence, and diff algorithms. State:dp[i][j] = length of the LCS of text1[:i] and text2[:j].
Transition: If the current characters match (text1[i-1] == text2[j-1]), extend the LCS from dp[i-1][j-1] + 1. Otherwise, the LCS is the better of skipping one character from either string: max(dp[i-1][j], dp[i][j-1]).
Walked example: text1 = “ace”, text2 = “abcde”. Build a 4x6 table. At (1,1): ‘a’==‘a’, dp=1. At (2,3): ‘c’==‘c’, dp=2. At (3,5): ‘e’==‘e’, dp=3. LCS = “ace”, length 3.
Time: O(m * n). Space: O(m * n) or O(min(m, n)) optimized (since each row depends only on the previous row).
5. House Robber (Cannot Take Adjacent)
A classic “cannot take adjacent” DP problem. A robber wants to maximize stolen money, but robbing two adjacent houses triggers an alarm. State:dp[i] = maximum money obtainable from houses 0..i.
Transition: At house i, you either rob it (take nums[i] plus the best from non-adjacent houses: dp[i-2] + nums[i]) or skip it (keep dp[i-1]). Take the max.
Walked example: nums = [2, 7, 9, 3, 1]. dp[0]=2, dp[1]=max(2,7)=7. dp[2]=max(7, 2+9)=11. dp[3]=max(11, 7+3)=11. dp[4]=max(11, 11+1)=12. Answer: 12 (rob houses 0, 2, 4).
Space optimization: Since dp[i] only depends on dp[i-1] and dp[i-2], two variables suffice — the same rolling-window trick as Climbing Stairs.
Time: O(n). Space: O(n) or O(1) optimized.
Common variation: House Robber II (circular) — run the algorithm twice, once excluding the first house and once excluding the last, then take the max.
6. Coin Change
An unbounded knapsack variant — each coin can be used unlimited times. The key difference from 0/1 knapsack: we iterate amounts forward (reuse is intentional). State:dp[i] = minimum number of coins needed to make amount i. Transition: For each amount i, try every coin: dp[i] = min(dp[i], dp[i - coin] + 1).
Edge case: If dp[amount] is still infinity, the amount cannot be formed — return -1.
Walked example: coins = [1, 3, 4], amount = 6. dp = [0, 1, 2, 1, 1, 2, 2]. Answer: 2 (coins 3+3).
Time: O(amount * len(coins)). Space: O(amount).
Classic Problems
1. Climbing Stairs
1. Climbing Stairs
2. Longest Increasing Subsequence
2. Longest Increasing Subsequence
3. Edit Distance
3. Edit Distance
4. Partition Equal Subset Sum
4. Partition Equal Subset Sum
Common Mistakes
Debugging Checklist
When your DP solution fails:DP Pattern Categories
| Category | Examples | State Definition |
|---|---|---|
| Linear DP | Climbing Stairs, House Robber | dp[i] = answer for first i elements |
| Grid DP | Unique Paths, Min Path Sum | dp[i][j] = answer reaching (i,j) |
| String DP | LCS, Edit Distance | dp[i][j] = answer for s1[:i], s2[:j] |
| Knapsack | 0/1 Knapsack, Coin Change | dp[i][w] = answer using i items, capacity w |
| Interval DP | Matrix Chain, Burst Balloons | dp[i][j] = answer for interval [i,j] |
| Tree DP | House Robber III | dp[node] = answer for subtree at node |
Complexity Quick Reference
| Problem Type | Time | Space | Optimization |
|---|---|---|---|
| 1D DP | O(n) | O(n) → O(1) | Keep only last 2 values |
| 2D Grid DP | O(mn) | O(mn) → O(n) | Keep only previous row |
| String DP (LCS) | O(mn) | O(mn) → O(n) | Keep only previous row |
| Knapsack | O(nW) | O(nW) → O(W) | Single row iteration |
| Interval DP | O(n³) | O(n²) | Usually not optimizable |
Interview Problems by Company
- Easy
- Medium
- Hard
| Problem | Company | DP Type |
|---|---|---|
| Climbing Stairs | All | 1D Linear |
| Min Cost Climbing Stairs | Amazon | 1D Linear |
| Maximum Subarray | All | 1D (Kadane’s) |
| Best Time to Buy Stock | Amazon | 1D State Machine |
| House Robber | Google, Meta | 1D Linear |
Interview Tips
How to Explain Your Approach
How to Explain Your Approach
- “I notice this is asking for [optimal/count], which suggests DP.”
- “Let me define the state: dp[i] represents…”
- “The recurrence is: dp[i] = … because…”
- “Base case is dp[0] = … because…”
- “Time is O(…) and space is O(…), which I can optimize to…”
When Interviewer Says...
When Interviewer Says...
| Interviewer Says | You Should Think |
|---|---|
| ”Find minimum/maximum” | Optimization DP |
| ”Count the number of ways” | Counting DP |
| ”Can you achieve X?” | Boolean DP (true/false) |
| “Optimize your recursion” | Add memoization |
| ”Reduce space complexity” | Rolling array technique |
Top-Down vs Bottom-Up
Top-Down vs Bottom-Up
- Easier to write (natural recursion)
- Only computes needed subproblems
- Can have stack overflow for large inputs
- Usually faster (no recursion overhead)
- Easier to optimize space
- Must compute all subproblems
How to Derive a DP Recurrence — The Five-Step Recipe
This is the single most important DP skill. Most failures are not “I cannot code DP,” they are “I cannot derive the recurrence.” Use these five steps every time, in this exact order:1. Define dp[i] (or dp[i][j]) in plain English first
dp[i] means before writing any code. Example: “dp[i] = the minimum number of coins to make amount i.” If you cannot finish that sentence cleanly, you have the wrong state.2. Write the recurrence by considering the last decision
dp[i]?” Enumerate every possible last move. Example for Coin Change: the last coin was some c from the coin set, so dp[i] = min(dp[i - c] + 1) over all c <= i.3. Identify the base case(s)
dp[0]? What about other small inputs? Coin Change: dp[0] = 0 (zero coins for amount 0). House Robber: dp[0] = nums[0], dp[1] = max(nums[0], nums[1]). A wrong base case shifts every later value.4. Decide the iteration order
dp[i] must be computed only after every state it depends on. For 1D DP this is usually left-to-right. For 2D DP it is row-by-row, left-to-right. For 0/1 knapsack with rolling array, capacity must iterate backwards. For interval DP, iterate by interval length, smallest first.DP Templates
Three reusable shapes covering the bulk of LeetCode DP problems.Top-Down (Memoization)
Bottom-Up Tabulation (1D)
Bottom-Up Tabulation (2D)
Space-Optimized 1D Rolling
Space-Optimized 2D Rolling (one row)
j backwards so you read the previous row’s value before overwriting it.
Worked LeetCode Problems
Eight canonical DP problems with full state definition, recurrence derivation, and code in multiple languages. If you can write all eight from scratch in fifteen minutes each, you are senior-ready on DP.LC 70 — Climbing Stairs (Easy, 1D linear)
Define:dp[i] = number of distinct ways to reach step i.
Recurrence: to reach step i, the last move was either +1 from i - 1 or +2 from i - 2. So dp[i] = dp[i-1] + dp[i-2].
Base: dp[0] = 1 (one way to “be” at the start), dp[1] = 1.
LC 198 — House Robber (Medium, 1D linear)
Define:dp[i] = max money robbed from the first i houses.
Recurrence: at house i, either rob it (dp[i-2] + nums[i]) or skip it (dp[i-1]). Take the max.
Base: dp[0] = nums[0], dp[1] = max(nums[0], nums[1]).
LC 322 — Coin Change (Medium, unbounded knapsack)
Define:dp[a] = minimum coins to make amount a. Use infinity for unreachable amounts.
Recurrence: the last coin was some c from coins, so dp[a] = min(dp[a - c] + 1) over all c <= a.
Base: dp[0] = 0.
Iteration order: amount outer (1..amount), coins inner. Forward direction is fine because each coin is reusable (unbounded knapsack).
LC 416 — Partition Equal Subset Sum (Medium, 0/1 knapsack)
Define:dp[s] = true if we can pick a subset summing to s using elements seen so far.
Recurrence: for each new element x, dp[s] = dp[s] OR dp[s - x] for all s from total // 2 down to x.
Base: dp[0] = True (empty subset).
Iteration order: elements outer; sum inner backwards — this is the textbook 0/1 vs unbounded distinction. Forward iteration would let one element contribute twice.
LC 1143 — Longest Common Subsequence (Medium, 2D string)
Define:dp[i][j] = length of LCS of text1[:i] and text2[:j].
Recurrence: if text1[i-1] == text2[j-1], the last characters match: dp[i][j] = dp[i-1][j-1] + 1. Otherwise: dp[i][j] = max(dp[i-1][j], dp[i][j-1]).
Base: dp[0][*] = dp[*][0] = 0 (empty string has LCS 0 with anything).
LC 300 — Longest Increasing Subsequence (Medium, two approaches)
Approach 1 — O(n^2) DP Define:dp[i] = length of the longest increasing subsequence ending at index i.
Recurrence: dp[i] = 1 + max(dp[j] for j < i if nums[j] < nums[i]), or 1 if no such j.
tails[k] = the smallest possible last element of an increasing subsequence of length k + 1. For each x, binary-search for the first tails[i] >= x and replace it with x. The length of tails at the end is the LIS length.
tails is strictly increasing. Replacing tails[i] with a smaller value keeps future LIS opportunities open. The actual elements of tails are not the LIS itself — only the length is correct. Reconstructing the LIS requires extra parent pointers.
LC 72 — Edit Distance (Medium-Hard, 2D string)
Define:dp[i][j] = minimum operations to convert word1[:i] into word2[:j].
Recurrence: if last characters match, dp[i][j] = dp[i-1][j-1] (free). Otherwise, take the min of three operations:
- Insert:
dp[i][j-1] + 1 - Delete:
dp[i-1][j] + 1 - Replace:
dp[i-1][j-1] + 1
dp[0][j] = j (insert j characters), dp[i][0] = i (delete i characters).
LC 5 — Longest Palindromic Substring (Medium, interval DP)
Define:dp[i][j] = True if s[i..j] is a palindrome.
Recurrence: dp[i][j] = (s[i] == s[j]) and (j - i < 2 OR dp[i+1][j-1]).
Iteration order: by length, smallest first. Lengths 1 and 2 are base cases.
Curated LeetCode List
Twelve problems grouped by sub-pattern. This is the minimum viable list for FAANG-grinding. If you have not seen one, schedule it.| LC # | Problem | Difficulty | Sub-category |
|---|---|---|---|
| 70 | Climbing Stairs | Easy | 1D linear (Fibonacci) |
| 198 | House Robber | Easy | 1D linear |
| 53 | Maximum Subarray | Easy | 1D Kadane’s |
| 91 | Decode Ways | Medium | 1D counting |
| 62 | Unique Paths | Medium | 2D grid |
| 64 | Minimum Path Sum | Medium | 2D grid |
| 322 | Coin Change | Medium | Unbounded knapsack |
| 416 | Partition Equal Subset Sum | Medium | 0/1 knapsack |
| 1143 | Longest Common Subsequence | Medium | 2D string |
| 300 | Longest Increasing Subsequence | Medium | LIS, both O(n^2) and O(n log n) |
| 72 | Edit Distance | Hard | 2D string |
| 5 | Longest Palindromic Substring | Medium | Interval DP |
| 312 | Burst Balloons | Hard | Interval DP |
| 337 | House Robber III | Medium | Tree DP |
| 691 | Stickers to Spell Word | Hard | Bitmask DP / BFS |
Practice Problems
| Problem | Difficulty | Link |
|---|---|---|
| Climbing Stairs | Easy | LeetCode 70 |
| House Robber | Medium | LeetCode 198 |
| Coin Change | Medium | LeetCode 322 |
| Longest Common Subsequence | Medium | LeetCode 1143 |
| Edit Distance | Medium | LeetCode 72 |
Practice Roadmap
Week 1: 1D DP
- Solve: Climbing Stairs, House Robber, Max Subarray
- Focus: State definition and transitions
Week 3: String DP
- Solve: LCS, Edit Distance, Palindrome Substring
- Focus: Two-string state definition
Interview Questions
Deep-dive questions that test real understanding of Dynamic Programming, not just template memorization. Expect interviewers to push past your initial answer into state design, optimization reasoning, and edge cases.Q1: You're given a recursive solution that's too slow. How do you determine if DP can help, and walk me through converting it step by step.
Q1: You're given a recursive solution that's too slow. How do you determine if DP can help, and walk me through converting it step by step.
- Two conditions must hold for DP to apply: (1) Optimal substructure — the optimal solution to the problem contains optimal solutions to subproblems, and (2) Overlapping subproblems — the recursion solves the same subproblem multiple times. If you have the first but not the second (e.g., merge sort), divide-and-conquer is sufficient. If you have neither, DP will not help.
- How I verify overlapping subproblems: Draw the recursion tree for a small input. If you see the same function call appearing in multiple branches, subproblems overlap. For Fibonacci,
fib(3)appears twice in the tree forfib(5). For merge sort, every recursive call operates on a unique subarray — no overlap. - Conversion steps: (1) Identify the recursive function parameters that change — these become your DP state. (2) Add a memo table (HashMap or array) keyed on those parameters. Before recursing, check the table; after computing, store the result. (3) Optionally, convert to bottom-up by determining the computation order from base cases to the final answer.
- The gotcha most candidates miss: Not every parameter needs to be part of the state. If a parameter is constant across all calls (like the original array), it is not part of the DP state. Only parameters that change define the subproblem space.
- Complexity impact: Memoization converts the time complexity from the number of nodes in the recursion tree (often exponential) to the number of unique subproblems times the work per subproblem. For Fibonacci: from O(2^n) to O(n) unique states times O(1) work each = O(n).
- Can you give me an example of a problem with optimal substructure but WITHOUT overlapping subproblems? (Answer: merge sort, binary search. These have optimal substructure but each subproblem is unique, so memoization adds no benefit — plain divide-and-conquer is the right approach.)
- When would you prefer top-down (memoization) over bottom-up (tabulation), and vice versa? Give a concrete scenario for each. (Answer: top-down when only a subset of states is reachable — e.g., a sparse state space where bottom-up would waste time computing unreachable states. Bottom-up when you need space optimization via rolling arrays, or when the recursion depth would cause a stack overflow for large inputs.)
Q2: In the 0/1 Knapsack space-optimized solution, why do we iterate capacity backwards? What breaks if we go forwards?
Q2: In the 0/1 Knapsack space-optimized solution, why do we iterate capacity backwards? What breaks if we go forwards?
- In the 2D knapsack,
dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i]] + value[i]). Notice both options reference rowi-1— the previous item’s row. When we compress to 1D, a singledp[w]array represents the “current” row, and we needdp[w - weight[i]]to still hold the value from the previous row. - If we iterate forwards (small w to large w), then when we compute
dp[w], the valuedp[w - weight[i]]has ALREADY been updated in this iteration for itemi. That means we are effectively readingdp[i][w - weight[i]]instead ofdp[i-1][w - weight[i]]. This allows us to pick itemimultiple times — which is the Unbounded Knapsack, not 0/1 Knapsack. - Iterating backwards ensures that when we read
dp[w - weight[i]], it still holds the value from the previous item’s computation (rowi-1). By the time we updatedp[w], all cells to its left that it depends on are still from the “old” row. - This is the single key insight that separates 0/1 Knapsack from Unbounded Knapsack in the space-optimized version. Same code, different iteration direction, completely different problems solved.
- If I now ask you to solve the Unbounded Knapsack (each item can be used unlimited times), what single change do you make? (Answer: iterate capacity forwards instead of backwards. This deliberately allows reading the current row’s values, which models reusing items.)
- What about the Bounded Knapsack where each item can be used at most K times? Can you still use a 1D array? (Answer: yes, but it is trickier. You can use binary representation — decompose K copies into groups of 1, 2, 4, … copies treated as separate items. This reduces to O(n * log(K) * W) time. Alternatively, use a sliding window maximum on the 1D array, but that is more complex to implement.)
Q3: Define the DP state for Edit Distance. Why is `dp[i][j]` defined as operations on prefixes, not suffixes or substrings?
Q3: Define the DP state for Edit Distance. Why is `dp[i][j]` defined as operations on prefixes, not suffixes or substrings?
dp[i][j]= minimum operations to convertword1[0..i-1]toword2[0..j-1](firsticharacters of word1 into firstjcharacters of word2).- Why prefixes work: Prefixes have a natural inductive structure. Once you know how to convert
word1[0..i-1]toword2[0..j-1], extending by one character gives you exactly three choices: insert (dp[i][j-1] + 1), delete (dp[i-1][j] + 1), or replace/match (dp[i-1][j-1] + 0 or 1). Each choice reduces the problem to a strictly smaller prefix pair. - Why suffixes would also work but are less natural: you could define
dp[i][j]as operations onword1[i..]toword2[j..], but the base cases becomedp[m][j] = n - janddp[i][n] = m - i, and the recurrence builds from the bottom-right corner. It is mathematically equivalent but harder to think about and code correctly. - Why arbitrary substrings do NOT work: If you define
dp[i][j]as the edit distance between arbitrary substrings, you have no way to decompose the problem without knowing the endpoints of the substrings. The state would need four parameters(i1, j1, i2, j2), which is O(n^2 * m^2) — far worse than O(n * m). - Base cases from the state definition:
dp[0][j] = j(insertingjcharacters to buildword2[0..j-1]from empty string),dp[i][0] = i(deletingicharacters fromword1[0..i-1]). These base cases fall out naturally from the prefix definition.
dp[i-1][j-1]” they have the operations mixed up.Follow-ups:- How would you reconstruct the actual sequence of operations (not just the minimum count)? (Answer: backtrack through the DP table from
dp[m][n]. At each cell, check which of the three transitions was chosen by comparing values. This takes O(m + n) additional time and reveals the edit script.) - Can you optimize Edit Distance to O(min(m, n)) space? What do you lose by doing so? (Answer: yes, use a rolling two-row array since each cell only depends on the current and previous row. You lose the ability to reconstruct the edit sequence — you would need to use Hirschberg’s algorithm, which combines divide-and-conquer with space-optimized DP to achieve O(min(m, n)) space AND O(mn) time while still allowing path reconstruction.)
Q4: Coin Change asks for the minimum number of coins. Coin Change II asks for the number of combinations. How does the DP formulation differ and why?
Q4: Coin Change asks for the minimum number of coins. Coin Change II asks for the number of combinations. How does the DP formulation differ and why?
- Coin Change (minimum coins):
dp[amount]= minimum coins to makeamount. Transition:dp[i] = min(dp[i], dp[i - coin] + 1)for each coin. We iterate amounts in the outer loop, coins in the inner loop (or vice versa — order does not matter for min). - Coin Change II (count combinations):
dp[amount]= number of ways to makeamount. Transition:dp[i] += dp[i - coin]. Here, iteration order is critical. To count combinations (not permutations), coins must be the OUTER loop and amounts the INNER loop. This ensures each coin denomination is considered in order, preventing counting[1, 2]and[2, 1]as different. - Why the loop order matters for counting:
- Coins outer, amounts inner: you process all amounts for coin 1, then all amounts for coin 2, etc. This enforces an ordering on coins used — you never “go back” to an earlier coin. Result: combinations.
- Amounts outer, coins inner: for each amount, you try all coins. A later coin can be followed by an earlier coin. Result: permutations.
- For min/max, loop order does not matter because
minandmaxare commutative and idempotent — regardless of the order you discover the paths, the minimum stays the same.
dp[0] = 0 for the counting version (it should be dp[0] = 1 — there is one way to make amount 0, which is using no coins).Follow-ups:- What if each coin can only be used once (like 0/1 Knapsack)? How does the solution change? (Answer: iterate coins in the outer loop and amounts BACKWARDS in the inner loop — same backward trick as 0/1 Knapsack. This prevents reusing the same coin.)
- The interviewer modifies the problem: “find the number of permutations that sum to target.” Now what changes? (Answer: swap the loops so amounts are outer, coins are inner. This is actually LeetCode 377 — Combination Sum IV, which despite its name counts permutations.)
Q5: Longest Increasing Subsequence has an O(n^2) DP solution and an O(n log n) solution. Explain both and when you'd choose each.
Q5: Longest Increasing Subsequence has an O(n^2) DP solution and an O(n log n) solution. Explain both and when you'd choose each.
- O(n^2) DP approach:
dp[i]= length of LIS ending at indexi. For eachi, scan allj < iwherenums[j] < nums[i]and takedp[i] = max(dp[j] + 1). Final answer ismax(dp). - O(n log n) approach (patience sorting): Maintain an array
tailswheretails[k]= smallest tail element of all increasing subsequences of lengthk + 1. For each element, binary searchtailsto find where it fits. If it is larger than all tails, append (extends the longest subsequence). Otherwise, replace the first tail that is >= the element (this keeps tails as small as possible for future extension). - Critical insight most people miss: The
tailsarray is NOT the LIS itself. It is always sorted, which is what enables binary search, but its contents at any point do not form a valid subsequence of the original array. To reconstruct the actual LIS, you need a separate parent-pointer array. - When to choose which:
- O(n^2): simpler to code, easier to explain, sufficient for n at most around 5000. Also easier to adapt for variants (e.g., longest non-decreasing subsequence, count of LIS, etc.).
- O(n log n): necessary when n is large (10^5 or more). But it is harder to modify for variants — for example, counting the number of LIS of maximum length is significantly more complex with the binary search approach.
tails array IS the LIS. This is a very common misconception. For input [3, 1, 4, 1, 5, 9, 2, 6], the tails array might be [1, 2, 5, 6] at the end, but the actual LIS is [1, 4, 5, 9] or [1, 4, 5, 6]. Another red flag: not knowing the O(n^2) solution exists and jumping straight to the binary search version without being able to explain the simpler DP first — interviewers prefer candidates who show progression from simple to optimized.Follow-ups:- How would you reconstruct the actual LIS (not just its length) from the O(n log n) approach? (Answer: maintain a parent array that records, for each element, the index of the previous element in its subsequence. When you place an element in
tails, record its predecessor. Then backtrack from the last element of the longest chain.) - How would you find the NUMBER of distinct longest increasing subsequences? Can the O(n log n) approach handle this efficiently? (Answer: with the O(n^2) approach, maintain a
count[i]alongsidedp[i]. Whendp[j] + 1 == dp[i], addcount[j]tocount[i]. With O(n log n), this requires augmenting the tails structure with Fenwick trees or segment trees — significantly more complex, O(n log n) is still achievable but the implementation difficulty jumps.)
Q6: What is interval DP and how does it differ from linear DP? Walk me through Burst Balloons or Matrix Chain Multiplication.
Q6: What is interval DP and how does it differ from linear DP? Walk me through Burst Balloons or Matrix Chain Multiplication.
- Linear DP builds solutions left to right:
dp[i]depends ondp[i-1],dp[i-2], etc. The subproblems are prefixes or suffixes. Interval DP builds solutions from smaller intervals to larger ones:dp[i][j]represents the answer for the range[i, j], and you try every possible split pointkin between. - The key pattern:
dp[i][j] = optimize over all k in [i, j] of (dp[i][k] + dp[k][j] + cost of combining). You iterate by interval length, starting from length 1 (or 2), building up to the full range. - Burst Balloons (LC 312) walkthrough:
- Reframe: instead of “which balloon to burst first,” think “which balloon to burst LAST in range
[i, j].” If balloonkis burst last in[i, j], the coins gained arenums[i-1] * nums[k] * nums[j+1](the boundaries are still intact becausekis the last to go). dp[i][j]= max coins from bursting all balloons in range[i, j].- Transition:
dp[i][j] = max over k in [i, j] of (dp[i][k-1] + nums[i-1] * nums[k] * nums[j+1] + dp[k+1][j]). - The non-obvious insight: thinking about the LAST element to remove rather than the first. This is because when you burst a balloon, it changes the neighbors of adjacent balloons — forward simulation is a nightmare. But if you think about which is burst last, the two sub-intervals
[i, k-1]and[k+1, j]are independent.
- Reframe: instead of “which balloon to burst first,” think “which balloon to burst LAST in range
- Complexity: O(n^3) time, O(n^2) space. The triple loop: interval length, start position, and split point.
dp[i] = “max coins after bursting balloon i” — this does not capture enough information because the answer depends on which other balloons are still present.Follow-ups:- In Matrix Chain Multiplication, the split point
kdetermines where you “cut” the chain. How is this similar to Burst Balloons, and what is the state? (Answer:dp[i][j]= minimum multiplications to compute the product of matricesithroughj. Split atk:dp[i][j] = min(dp[i][k] + dp[k+1][j] + dims[i-1] * dims[k] * dims[j]). Same interval DP structure.) - Can interval DP be done top-down with memoization instead of bottom-up? What are the trade-offs? (Answer: yes, and for Burst Balloons specifically, top-down is often easier to write because you do not need to think about the iteration order by interval length. The trade-off is recursion depth and potential stack overflow for large n, plus slightly higher constant factor from function call overhead.)
Q7: How do you optimize the space complexity of a 2D DP solution? Walk me through the rolling array technique and when it fails.
Q7: How do you optimize the space complexity of a 2D DP solution? Walk me through the rolling array technique and when it fails.
- The core principle: Look at which rows (or columns) each cell depends on. If
dp[i][j]only depends on rowiand rowi-1, you only need two rows at any time — a “current” and a “previous” row. This reduces O(mn) space to O(n). - Rolling array technique: Use
dp[i % 2][j]instead ofdp[i][j]. Row 0 and row 1 alternate as “current” and “previous.” After processing rowi, rowi-2is no longer needed and can be overwritten. - Single-row optimization (even better): In some problems like Unique Paths, you can use a single 1D array where
dp[j]represents the current row being built. When you updatedp[j] += dp[j-1], thedp[j]on the right side still holds the value from the previous row (which is the “up” neighbor), anddp[j-1]has already been updated to the current row (which is the “left” neighbor). This works because the dependency is only ondp[i-1][j]anddp[i][j-1]. - When it FAILS:
- When the cell depends on more than two rows (e.g.,
dp[i][j]depends ondp[i-1],dp[i-2], etc.). You need to keep more rows. - When you need to reconstruct the solution path (not just the optimal value). Space optimization destroys the full table, so you cannot backtrack. You would need Hirschberg’s divide-and-conquer trick to get both O(n) space and path reconstruction.
- Interval DP:
dp[i][j]depends on arbitrary sub-intervals, not just adjacent rows. Rolling arrays do not help here.
- When the cell depends on more than two rows (e.g.,
- In the LCS space optimization with a single row, you need a temporary variable to hold
dp[j-1]from the previous row before it gets overwritten. Walk me through why. (Answer: when computingdp[j], you needprev[j-1]— the diagonal. Butdp[j-1]has already been overwritten to the current row’s value. You need to save it in atempvariable before the update. The two-row approach avoids this complexity.) - If your interviewer says “I also need the actual LCS string, not just the length, but still in O(n) space” — what do you do? (Answer: Hirschberg’s algorithm. Divide the first string in half. Use forward DP on the first half and backward DP on the second half to find the optimal split point in the second string. Recurse on each half. Total: O(mn) time, O(n) space, and you get the full path.)
Q8: Explain how the state machine DP approach works for Best Time to Buy and Sell Stock with Cooldown.
Q8: Explain how the state machine DP approach works for Best Time to Buy and Sell Stock with Cooldown.
- The mental model: On each day, you are in one of three states: held (own a stock), sold (just sold today, in cooldown tomorrow), rest (not holding and not in cooldown). The DP tracks the maximum profit in each state on each day.
- Transitions:
held[i] = max(held[i-1], rest[i-1] - prices[i])— either keep holding, or buy today (can only buy from rest state, not sold/cooldown state).sold[i] = held[i-1] + prices[i]— sell what you are holding.rest[i] = max(rest[i-1], sold[i-1])— either keep resting, or transition from cooldown (sold yesterday, forced to rest today).
- Base cases:
held[0] = -prices[0],sold[0] = 0,rest[0] = 0. - Answer:
max(sold[n-1], rest[n-1])— you should not end holding a stock. - Why state machine is powerful: The same framework handles all stock variants. No cooldown? Remove the rest state. Transaction limit K? Add a transaction counter to the state. Transaction fee? Subtract fee in the sell transition. The framework is modular.
dp[i] = max profit on day i without tracking whether you are holding a stock. This collapses the state space and makes transitions impossible to define correctly. Another red flag: not realizing that the cooldown constraint means you cannot buy the day after selling, and trying to handle it with ad-hoc conditionals instead of clean state separation.Follow-ups:- How would you extend this to the “at most K transactions” variant (LC 188)? What changes in the state? (Answer: add a transaction counter:
held[i][k]andrest[i][k]. Buying starts a new transaction, soheld[i][k] = max(held[i-1][k], rest[i-1][k-1] - prices[i]). This is O(nK) time and can be optimized to O(K) space. When K >= n/2, it degenerates to the unlimited-transactions case.) - Can you draw the state machine diagram for the “with cooldown” variant and prove there are no missing transitions? (Answer: three states, five edges.
rest -> held(buy),held -> held(hold),held -> sold(sell),sold -> rest(forced cooldown),rest -> rest(wait). No edge fromsold -> heldorsold -> sold— that is the cooldown constraint.)
Q9: What is the difference between top-down and bottom-up DP? When does one fail but the other succeed?
Q9: What is the difference between top-down and bottom-up DP? When does one fail but the other succeed?
- Top-down (memoization): Write the natural recursion, add a cache. You solve only the subproblems that are actually needed (lazy evaluation). Pros: (1) easier to write since you think recursively, (2) skips unreachable states automatically. Cons: (1) recursion overhead and stack depth limit — Python defaults to 1000 frames, and even Java/C++ can overflow for large inputs, (2) harder to optimize space, (3) HashMap-based memo has higher constant factor than array access.
- Bottom-up (tabulation): Fill a table iteratively from base cases to the answer. You must compute ALL subproblems in the right order. Pros: (1) no recursion overhead, (2) enables rolling-array space optimization, (3) better cache locality since you iterate sequentially through memory. Cons: (1) you must figure out the correct iteration order, (2) may compute states that are never needed.
- When top-down succeeds but bottom-up is painful: Sparse state spaces. If your state is
dp[i][j][k]where only a small fraction of(i, j, k)triples are reachable, top-down with a HashMap only visits those states. Bottom-up would allocate and iterate through the full 3D array. - When bottom-up succeeds but top-down fails: Large input sizes where recursion depth exceeds the stack limit. For example, a DP with n = 10^6 states would crash with a stack overflow in top-down Python. Bottom-up iterates without any stack concern.
- In interviews, my strategy: Start with top-down (easier to get correct), then convert to bottom-up if the interviewer asks for space optimization or if the input size is large.
- In Python, how would you handle a top-down DP with n = 500,000 states? (Answer: increase the recursion limit with
sys.setrecursionlimit, but this risks crashing the process. The real answer is to convert to bottom-up. Alternatively, use iterative DFS with an explicit stack to simulate the recursion.) - Can you think of a problem where top-down is asymptotically faster than bottom-up? (Answer: yes — when the state space is sparse and only a fraction of states are reachable. For example, in a DP over a grid with obstacles where most cells are blocked, top-down only visits reachable cells while bottom-up processes the entire grid.)
Q10: Walk me through how you'd solve Word Break: given a string and a dictionary, determine if the string can be segmented into dictionary words.
Q10: Walk me through how you'd solve Word Break: given a string and a dictionary, determine if the string can be segmented into dictionary words.
- State:
dp[i]= True if the substrings[0..i-1](firsticharacters) can be segmented into dictionary words. The answer isdp[n]. - Transition:
dp[i]is True if there exists somej < isuch thatdp[j]is True ANDs[j..i-1]is in the dictionary. In other words, we can segmented up to positionj, and the remainder fromjtoiis a valid word. - Base case:
dp[0] = True(the empty string is trivially segmentable). - Optimization: Convert the dictionary to a HashSet for O(1) lookups. Also, you can limit the inner loop: instead of checking all
jfrom 0 toi-1, only check positions wherei - jis at most the length of the longest dictionary word. This prunes significantly. - Complexity: O(n^2 * L) where n is the string length and L is the average word length (for substring creation and hash comparison). With the max-word-length optimization, it becomes O(n * maxLen * L) in practice.
- Why not backtracking? Pure backtracking without memoization is exponential. Consider the string “aaaaab” with dictionary
["a", "aa", "aaa", ...]— every partition of the prefix is valid but the string ultimately cannot be segmented (no word ends with “b”). Backtracking explores exponentially many dead ends. DP avoids this by caching results.
dp[i] = “whether the substring starting at i can be segmented” without being able to code it correctly (confusing 0-indexed vs 1-indexed boundaries is extremely common here). Also watch for candidates who use a Trie for the dictionary but cannot explain what advantage it provides over a HashSet — a Trie helps when you are checking many prefixes from the same starting position, but for Word Break, a HashSet with the max-length optimization is usually simpler and sufficient.Follow-ups:- How would you modify this to return ALL possible segmentations (Word Break II, LC 140)? Does the DP approach still work? (Answer: use top-down DP with backtracking to reconstruct all paths. The key is that the number of valid segmentations can be exponential — e.g., “aaa” with dictionary
["a", "aa", "aaa"]has 4 segmentations — so the output itself is exponential. Memoize the list of valid segmentations from each starting position.) - What if the dictionary is extremely large (millions of words) but the input string is short (length 100)? Does your approach change? (Answer: no, because we only look up substrings of the input in the dictionary. The number of lookups is O(n * maxLen), independent of dictionary size. The HashSet lookup is O(L) where L is the substring length, not affected by dictionary size.)
Interview Deep-Dive
How do you identify that a problem requires DP rather than greedy or backtracking? Walk through your decision framework on an unfamiliar problem.
How do you identify that a problem requires DP rather than greedy or backtracking? Walk through your decision framework on an unfamiliar problem.
- Step 1 — Check for optimal substructure. Does the optimal solution contain optimal solutions to subproblems? If not, neither greedy nor DP applies.
- Step 2 — Check for overlapping subproblems. Are the same subproblems solved multiple times? If yes, DP. If subproblems are independent, D&C. If locally optimal choices suffice, greedy.
- Step 3 — Try to break greedy. Spend 2-3 minutes constructing a counterexample. For coin change with coins [1, 3, 4] and target 6: greedy picks 4+1+1=3 coins, but optimal is 3+3=2. Greedy fails, so DP is needed.
- Step 4 — Define the state. Articulate what
dp[i]ordp[i][j]represents. If you cannot cleanly define the state, you may need a different formulation. - Key signals for DP: “minimum cost,” “maximum profit,” “number of ways,” “can you achieve X” with choices at each step.
- Key signals against DP: “generate all solutions” (backtracking), “single irrevocable choice per step” (greedy), “no overlapping subproblems” (D&C).
- For min/max, the function is commutative and idempotent — order does not affect the result. For counting, processing amounts before coins allows [1,2] and [2,1] to be counted separately (permutations). Processing coins first enforces an ordering, counting only combinations.
Explain the rolling array space optimization for 2D DP. When does it work, when does it fail, and what do you lose?
Explain the rolling array space optimization for 2D DP. When does it work, when does it fail, and what do you lose?
- When it works: When
dp[i][j]depends only on rowiand rowi-1. Keep two rows and alternate. Reduces O(mn) space to O(n). - Single-row optimization: For Unique Paths,
dp[j] += dp[j-1]works becausedp[j]on the right still holds the previous row’s value anddp[j-1]is already updated to the current row. - When it fails: (1) Cell depends on more than two rows. (2) Interval DP — dependencies span arbitrary sub-intervals. (3) Path reconstruction needed — rolling arrays destroy the full table.
- What you lose: The ability to backtrack through the table. For “just the value” problems, no loss. For “print the actual path,” use Hirschberg’s algorithm (D&C + rolling array) for O(n) space with reconstruction.
- 0/1 Knapsack subtlety: Iterate the inner loop backwards. Forward iteration reuses updated cells, turning 0/1 into unbounded knapsack.
- When computing
dp[j], you need the diagonal value from the previous row (old dp[j-1]). Butdp[j-1]was already overwritten. Save olddp[j]in a temp variable before updating — it becomes the next iteration’s diagonal.
Compare top-down memoization versus bottom-up tabulation. When does each shine, and when does it fail?
Compare top-down memoization versus bottom-up tabulation. When does each shine, and when does it fail?
- Top-down: Easier to write (add cache to recursion). Only computes reachable states. Natural for tree DP. Fails on deep recursion (Python limit ~1000). HashMap memo has higher constant factor than array access.
- Bottom-up: No recursion overhead. Better cache locality. Enables rolling-array optimization. Must figure out iteration order. Computes ALL states, even unreachable ones.
- Top-down shines: Sparse state spaces (bitmask DP), tree problems, quick prototyping.
- Bottom-up shines: Dense state spaces (grid/string DP), large inputs, space optimization needed.
- Interview strategy: Start top-down (faster to code correctly), convert to bottom-up if asked for optimization.
- When the state space is sparse. A grid DP with 90% blocked cells: top-down visits only reachable cells (10% of grid), while bottom-up processes everything. The asymptotic difference can be significant.
The state machine DP approach models stock trading. How does adding cooldown, transaction limits, or fees change the transitions?
The state machine DP approach models stock trading. How does adding cooldown, transaction limits, or fees change the transitions?
- Base (unlimited transactions): States: held, rest.
held[i] = max(held[i-1], rest[i-1] - price),rest[i] = max(rest[i-1], held[i-1] + price). - With cooldown: Add sold state.
sold[i] = held[i-1] + price,rest[i] = max(rest[i-1], sold[i-1]),held[i] = max(held[i-1], rest[i-1] - price). Cooldown prevents held from transitioning from sold — must go through rest. - With K transactions: Add counter:
held[i][k],rest[i][k]. Buying starts transaction k. Time: O(nK). When K >= n/2, constraint is lifted. - With fee: Subtract fee on sell:
rest[i] = max(rest[i-1], held[i-1] + price - fee). - Why state machine works: Each variant adds or modifies one state/transition. The framework is modular.
- Space: All variants optimize to O(1) or O(K) since each day depends only on the previous day.
- Three states, five edges: rest->held (buy), held->held (hold), held->sold (sell), sold->rest (forced cooldown), rest->rest (wait). No edge from sold->held or sold->sold — that is the cooldown.
LeetCode Interview Q&A
Five LeetCode-grinder-flavored DP questions you should be able to answer cold. Each one comes with a strong-answer framework, full code (top-down and bottom-up where relevant), senior follow-ups, common wrong answers, and further reading.Q1: Coin Change (LC 322) -- explain top-down and bottom-up approaches and their trade-offs.
Q1: Coin Change (LC 322) -- explain top-down and bottom-up approaches and their trade-offs.
- Define state:
dp[a]= minimum coins to make amounta. - Recurrence: the last coin used was some
c, sodp[a] = min(dp[a - c] + 1)over allcat mosta. - Base case:
dp[0] = 0. Sentinel:infinity(oramount + 1) for unreachable. - Iteration order: amount outer (1..amount), coins inner. Forward direction works because each coin is reusable.
- Both top-down and bottom-up are O(amount * coins) time and O(amount) space.
coins = [1, 2, 5], amount = 11: dp = [0, 1, 1, 2, 2, 1, 2, 2, 3, 3, 2, 3]. Answer: 3 (5+5+1).parent[a] = c alongside dp[a], recording which coin gave the best result. To reconstruct, start at amount, repeatedly subtract parent[a] and prepend it. Adds O(amount) extra space.dp[a] += dp[a - c]. Critically, the loop order changes: coins outer, amount inner. With amount outer, you would count permutations (every ordering of the same coin set), which is LC 377 — a different problem. The sub-pattern: “combinations: outer item; permutations: outer position.”dp[a - c] can require unboundedly large amounts (e.g., to reach a = 5 you could go through a = 1000 first if you have a -995 coin). The problem becomes ill-posed without an upper bound. With a bound, it becomes shortest-path on a graph — solve with BFS or Dijkstra.- Greedy “always pick the largest coin.” Fails on
coins = [1, 3, 4], amount = 6(greedy says 3, optimal is 2). - Top-down without memoization. Exponential, times out.
- Forgetting to return -1 for unreachable amounts (sentinel handling).
- Confusing LC 322 (minimum count) with LC 518 (number of ways) — different recurrences.
- LC 518 — Coin Change II (counting variant)
- LC 377 — Combination Sum IV (permutations variant)
- LC 983 — Minimum Cost For Tickets
Q2: Longest Increasing Subsequence (LC 300) -- give both O(n^2) DP and O(n log n) patience sorting.
Q2: Longest Increasing Subsequence (LC 300) -- give both O(n^2) DP and O(n log n) patience sorting.
- Start with the natural O(n^2) DP:
dp[i]= length of LIS ending at indexi. Recurrence:dp[i] = 1 + max(dp[j])overj < iwithnums[j] < nums[i]. - Show that the answer is
max(dp), notdp[n - 1]— the LIS does not have to end at the last element. - For the O(n log n) version, introduce patience sorting: maintain
tails[k]= smallest possible last element of an increasing subsequence of lengthk + 1. For eachx, binary-search and replace. - Length of
tailsat the end equals LIS length. Note:tailsis not the LIS itself.
nums = [10, 9, 2, 5, 3, 7, 101, 18]:- 10 -> tails = [10]
- 9 -> tails = [9]
- 2 -> tails = [2]
- 5 -> tails = [2, 5]
- 3 -> tails = [2, 3]
- 7 -> tails = [2, 3, 7]
- 101 -> tails = [2, 3, 7, 101]
- 18 -> tails = [2, 3, 7, 18]
[2, 3, 7, 18] or [2, 3, 7, 101] — the tails array does not literally hold the LIS, only the length.bisect_left for strictly increasing (LIS); bisect_right for non-decreasing (longest non-decreasing subsequence). Confusing them silently produces the wrong answer on inputs with equal elements. LC 300 wants strictly increasing, so bisect_left is correct.x, record the index of the predecessor (the element at tails[i - 1] at the time x was inserted). Walk backwards from the last inserted element. Adds O(n) space.dp[i][k] = LIS ending at i using at most k removals. Recurrence considers two cases per j < i: keep j (require nums[j] < nums[i]) or remove j (just inherit dp[j-1][k-1]). O(n^2 * K) time.- Returning
dp[n - 1]instead ofmax(dp)— common off-by-one trap. - Using
bisect_rightfor strict increase — wrong for inputs with duplicates. - Trying to make the patience sorting array hold the actual LIS — it does not; it only tracks length.
- Recursive without memoization — exponential.
- LC 354 — Russian Doll Envelopes (2D LIS, sort then 1D LIS)
- LC 673 — Number of Longest Increasing Subsequences
- LC 1671 — Min Removals to Make Mountain Array
Q3: Edit Distance (LC 72) -- derive the recurrence from first principles.
Q3: Edit Distance (LC 72) -- derive the recurrence from first principles.
- Define:
dp[i][j]= minimum operations to convertword1[:i]toword2[:j]. - Reason about the last operation. Three possibilities (plus a free case):
- Free: if
word1[i-1] == word2[j-1], last characters match, no cost:dp[i][j] = dp[i-1][j-1]. - Replace: change
word1[i-1]toword2[j-1]:dp[i-1][j-1] + 1. - Delete: drop
word1[i-1]:dp[i-1][j] + 1. - Insert: insert
word2[j-1]at the end ofword1[:i]:dp[i][j-1] + 1.
- Free: if
- Base cases:
dp[0][j] = j(insert j chars from empty),dp[i][0] = i(delete all i chars). - Iteration order: row by row, left to right. Each cell depends on three already-computed cells.
- Complexity: O(m * n) time and space; can drop to O(min(m, n)) space with rolling rows.
word1 = "horse", word2 = "ros". Final dp[5][3] = 3. Path: horse -> rorse (replace h with r) -> rose (delete r) -> ros (delete e). Three ops.dp[i][j], you need the previous row’s dp[i-1][j-1], but if you update in place left-to-right with one row, you have already overwritten that cell. Cache it in a temp variable before overwriting.dp[i][j] = min(dp[i-1][j-1] + replace_cost, dp[i-1][j] + delete_cost, dp[i][j-1] + insert_cost). The structure is unchanged because each operation still moves you one cell in the grid.dp[m][n]. At each cell, look at which neighbor produced the minimum and record the operation. Stop when both indices reach 0. O(m + n) time, O(1) extra (besides the table).- Forgetting the “free” case when characters match — doubles the answer.
- Wrong base case (
dp[0][0] = 1or similar). Empty-to-empty is 0 ops, not 1. - Using only two operations (replace and one of insert/delete) — gives a wrong but plausible answer.
- LC 583 — Delete Operations for Two Strings (only delete allowed)
- LC 712 — Minimum ASCII Delete Sum
- LC 1143 — Longest Common Subsequence (related but simpler)
Q4: 0/1 Knapsack -- show why iteration order of capacity matters when using a 1D array.
Q4: 0/1 Knapsack -- show why iteration order of capacity matters when using a 1D array.
- State the 2D version first:
dp[i][w]= max value using items0..i-1with capacityw. - Recurrence: take or skip item
i-1.dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i-1]] + value[i-1])if it fits. - Observe that row
idepends only on rowi-1. So compress to 1D. - Critical: when iterating with the 1D array, capacity must go backwards from
Wdown toweight[i-1]. Forward iteration would use the current row’sdp[w - weight[i-1]], which already includes itemi-1, accidentally allowing it twice — turning 0/1 into unbounded knapsack. - Walk through a tiny example to demonstrate the bug and the fix.
weights = [2], values = [3], W = 4.With backwards iteration: dp starts [0, 0, 0, 0, 0]. Item 0 (w=2, v=3):- w=4:
dp[4] = max(0, dp[2] + 3) = max(0, 0 + 3) = 3. - w=3:
dp[3] = max(0, dp[1] + 3) = max(0, 0 + 3) = 3. - w=2:
dp[2] = max(0, dp[0] + 3) = max(0, 0 + 3) = 3. - Final dp = [0, 0, 3, 3, 3]. Answer dp[4] = 3. Correct.
dp starts [0, 0, 0, 0, 0]. Item 0:- w=2:
dp[2] = max(0, dp[0] + 3) = 3. - w=3:
dp[3] = max(0, dp[1] + 3) = 3. - w=4:
dp[4] = max(0, dp[2] + 3) = 6. Wrong! Item used twice. - Final dp[4] = 6. This is the unbounded answer, not 0/1.
dp[i][*] is a separate row from dp[i-1][*]. The recurrence reads only from row i-1, never from row i mid-update. The bug emerges only when you compress rows.total // 2. The 0/1 backward-iteration discipline is identical:dp[n][W]. If dp[i][w] != dp[i-1][w], item i-1 was taken; subtract its weight and continue. The 1D rolling-array version loses this information — you trade space for traceability.- Forward iteration on the 1D array — silently solves the wrong problem.
- Using a 2D array but reading from the current row instead of the previous — same bug, harder to spot.
- Running greedy by value-per-weight ratio — works for fractional knapsack, not 0/1.
- Forgetting the
weights[i] - 1lower bound — attempts to access negative indices.
- LC 416 — Partition Equal Subset Sum
- LC 494 — Target Sum (0/1 with sign decisions)
- LC 1049 — Last Stone Weight II (0/1 in disguise)
Q5: Longest Palindromic Substring (LC 5) -- contrast interval DP and expand-around-center.
Q5: Longest Palindromic Substring (LC 5) -- contrast interval DP and expand-around-center.
- Define DP state:
dp[i][j]= True iffs[i..j]is a palindrome. - Recurrence:
dp[i][j] = (s[i] == s[j])AND (j - i < 2ORdp[i+1][j-1]). - Iteration order: by length, smallest first. Inside each length, slide
ifrom 0 ton - length. - Both DP and expand-around-center are O(n^2). DP uses O(n^2) space; expand uses O(1).
- For the truly optimal O(n) algorithm, mention Manacher — usually not required to code in interviews, but knowing the name signals depth.
s = "babad": expand-around-center gives “bab” (or “aba” — both valid). DP version fills the table from length 1 up to 5; same answer.dp[i][j] = LPS of s[i..j]. Recurrence: if s[i] == s[j] then dp[i+1][j-1] + 2, else max(dp[i+1][j], dp[i][j-1]). This is essentially LCS of s and reverse(s).- Iterating the DP table by
iouter andjinner — you would accessdp[i+1][j-1]before computing it. Iteration order must be by length or by diagonal. - Returning just the length when the problem asks for the substring.
- Confusing substring (contiguous) with subsequence (LC 516, non-contiguous).
- Using brute force O(n^3) — check every substring with a palindrome test. Times out on
n = 1000.
- LC 516 — Longest Palindromic Subsequence
- LC 647 — Palindromic Substrings (count)
- LC 132 — Palindrome Partitioning II (DP plus interval)