Dynamic programming
Overlapping subproblems. Two questions solve almost everything: what is the state, and what is the transition?
Trigger in the prompt: 'How many ways', 'minimum cost to', 'longest such-and-such'.
easyClimbing Stairs
Count the ways to climb n steps taking one or two at a time.
Trigger'How many ways' plus a small fixed set of moves.
Approach- Write the recurrence out loud: ways(n) = ways(n−1) + ways(n−2).
- Base cases: ways(0) = ways(1) = 1.
- It is Fibonacci — say so, it shows you recognise the shape.
- Two variables instead of an array: O(1) space.
O(n) time, O(1) space.
PitfallGetting the base cases wrong. Check n = 1 and n = 2 by hand before running.
Say it out loud“The last step was either one or two, so the count is the sum of the two previous counts — this is Fibonacci. I only ever look one and two steps back, so I'll keep two variables instead of an array and get constant space.”
Solve on LeetCodeeasyCounting Bits
Return the set-bit count for every integer from 0 to n.
Trigger'For every number up to n' — a DP over bit patterns.
Approach- Calling popcount n times is O(n log n). Say it, then improve.
- DP: dp[i] = dp[i >> 1] + (i & 1). Shifting right removes the last bit, which I add back.
- The subproblem is always smaller, so a single forward pass works.
O(n) time, O(n) space for the output.
PitfallNot recognising that this is DP. Recomputing popcount per number is the answer that scores lower.
Say it out loud“This is DP over bit patterns: the count for i is the count for i shifted right, plus its last bit. Every subproblem is strictly smaller, so one forward pass fills the table — linear rather than n log n.”
Solve on LeetCodeeasyPascal's Triangle
Generate the first n rows of Pascal's triangle.
Trigger'Pascal', 'binomial coefficients', 'each element is the sum of the two above'.
Approach- The recurrence is the definition: C(n,k) = C(n−1,k−1) + C(n−1,k).
- Build each row from the previous one, with ones at both ends.
- Connect it to combinatorics: row n is the binomial coefficients.
O(n²) time and space — the output itself is quadratic.
PitfallOff-by-one at the row edges. Trace row three by hand before you run it.
Say it out loud“This is the binomial recurrence written out, so each row builds from the previous one. Worth noting the output itself is quadratic in size, so O(n squared) is the floor here, not a weakness of the approach.”
Solve on LeetCodemediumCoin Change
Find the minimum number of coins summing to an amount, with unlimited coins of each type.
Trigger'Fewest coins', 'minimum items to reach a total' — unbounded knapsack.
Approach- State: dp[a] = fewest coins to make amount a. Initialise to infinity, dp[0] = 0.
- Transition: for each amount, try every coin — dp[a] = min(dp[a], dp[a − c] + 1).
- Say why greedy fails: coins [1, 3, 4] and amount 6 gives 4+1+1 greedily but 3+3 is optimal.
O(amount × coins) time, O(amount) space.
PitfallReaching for greedy. It only works for canonical coin systems, and the interviewer will pick one where it does not.
Say it out loud“Greedy would take the largest coin first, but that's only optimal for canonical systems — with coins one, three and four and a target of six, greedy gives three coins and the answer is two. So it's DP over the amount, and the loop order makes each coin reusable, which is what unbounded means.”
Solve on LeetCodemediumDecode Ways
Count the ways to decode a digit string where 1–26 map to letters.
Trigger'How many decodings', 'ways to parse a string with a mapping'.
Approach- State: dp[i] = decodings of the prefix of length i.
- Transition: add dp[i−1] if the single digit is valid (not zero), and dp[i−2] if the two-digit number is 10 to 26.
- Zeros are the whole difficulty: '0' alone is invalid, '10' and '20' are valid, '30' is not.
O(n) time, O(1) space with two variables.
PitfallMishandling zeros. Test '0', '06', '10', '100' by hand — that is where it breaks.
Say it out loud“It's Fibonacci-shaped with validity conditions on each term. The real work is the zeros: a zero can never stand alone, and it's only valid as the second digit of ten or twenty. I'll trace those cases by hand before I claim it's done.”
Solve on LeetCodemediumEdit Distance
Find the minimum number of insertions, deletions and substitutions to turn one string into another.
Trigger'Edit distance', 'minimum operations to transform', spell-checking.
Approach- State: dp[i][j] = distance between the two prefixes.
- Base cases: transforming from or to an empty string costs the other length.
- Transition: if the characters match, carry dp[i−1][j−1]; otherwise 1 + min of the three neighbours, and name which operation each neighbour is.
- Row reduction to O(m) space.
O(n × m) time, O(min(n, m)) space.
PitfallNot initialising the first row and column. It is the most common DP bug there is.
Say it out loud“Each cell is the cost of aligning two prefixes. The three neighbours map onto the three operations — delete, insert, substitute — so the transition writes itself once the state is clear. The part I'll be careful with is the base row and column, which encode transforming to and from the empty string.”
Solve on LeetCodemediumHouse Robber
Maximise the sum of a subsequence with no two adjacent elements.
Trigger'Cannot take two neighbours', 'maximise without adjacency'.
Approach- State: dp[i] = the best achievable considering the first i houses.
- Transition: dp[i] = max(dp[i−1], dp[i−2] + value[i]) — skip it or take it.
- Roll the array down to two variables.
O(n) time, O(1) space.
PitfallAssuming greedy works. Taking every other house is not optimal — [2, 1, 1, 2] proves it.
Say it out loud“The decision at each house is take it or skip it, and taking it forbids the previous one. That gives me a two-term recurrence, so it's linear time and I can roll it into two variables. Greedy fails here — two-one-one-two is the counterexample.”
Solve on LeetCodemediumJump Game
Decide whether you can reach the last index, given a maximum jump length at each position.
Trigger'Can you reach the end', 'maximum jump at each step'.
Approach- DP works: reachable[i] from any earlier reachable j with reach — O(n²).
- Greedy is better: track the furthest index reachable so far; if i ever exceeds it, fail.
- This is one of the cases where greedy provably beats DP — say why.
O(n) time, O(1) space greedily.
PitfallWriting the O(n²) DP and stopping there. The greedy is the expected answer.
Say it out loud“The DP is quadratic, but here greedy is provably correct: I sweep left to right tracking the furthest reachable index, and I fail the moment my position passes it. The exchange argument is that reaching further is never worse, so there's no reason to prefer a shorter jump.”
Solve on LeetCodemediumLongest Common Subsequence
Find the length of the longest subsequence common to two strings.
TriggerTwo strings plus 'common', 'shared', 'alignment'.
Approach- State: dp[i][j] = LCS of the first i characters of A and the first j of B.
- Transition: if the characters match, 1 + dp[i−1][j−1]; otherwise max of dropping one side.
- Row-by-row space reduction to O(min(n, m)).
O(n × m) time, O(min(n, m)) space after the reduction.
PitfallConfusing subsequence with substring. Subsequences need not be contiguous.
Say it out loud“The state is the pair of prefixes. If the two characters match I take them and move both pointers; otherwise I take the better of dropping one. Each row only depends on the previous one, so I can drop the table to two rows.”
Solve on LeetCodemediumLongest Increasing Subsequence
Find the length of the longest strictly increasing subsequence.
Trigger'Longest increasing subsequence', or anything reducible to it.
Approach- O(n²) DP first: dp[i] = the best ending at i, scanning all j < i.
- Then the O(n log n) version: keep an array of the smallest tail for each length.
- For each value, binary search the position it replaces. The array's length is the answer.
- Be explicit: that array is not the subsequence, only its lengths.
O(n log n) time, O(n) space.
PitfallClaiming the tails array is the actual subsequence. It is not; reconstructing needs parent pointers.
Say it out loud“The straightforward DP is quadratic. The n-log-n version keeps, for each length, the smallest possible tail — and binary searches where each new value belongs. Worth saying: that array isn't a valid subsequence, it's a set of best-case tails, so reconstructing the actual sequence needs back-pointers.”
Solve on LeetCodemediumMaximum Subarray
Find the contiguous subarray with the largest sum.
Trigger'Maximum contiguous sum', values can be negative.
Approach- Brute force O(n²) over all start-end pairs. Say it, then improve.
- Kadane: at each element, either extend the current run or start fresh from here.
- cur = max(x, cur + x); best = max(best, cur).
- Mention divide and conquer as the O(n log n) alternative, since it is the follow-up.
O(n) time, O(1) space.
PitfallInitialising `best` to zero. With an all-negative array the answer is the least negative element, not zero.
Say it out loud“This is Kadane, which is really DP with a one-element state: the best sum ending here is either this element alone or this element plus the best ending before it. I'll initialise from the first element rather than zero, so an all-negative array still gives the right answer.”
Solve on LeetCodemediumPartition Equal Subset Sum
Decide whether an array can be split into two subsets with equal sums.
Trigger'Split into two equal halves', 'is there a subset summing to X' — subset sum.
Approach- Total must be even, otherwise return false immediately.
- Reduce to: is there a subset summing to total/2? That is subset sum, which is NP-complete.
- Pseudo-polynomial DP: a boolean array over reachable sums, iterated downwards per item.
- Say the iteration direction and why: descending prevents reusing the same item twice.
O(n × sum) time, O(sum) space.
PitfallIterating the inner loop upwards, which silently turns 0/1 knapsack into unbounded knapsack.
Say it out loud“This is subset sum, which is NP-complete, but the DP is pseudo-polynomial in the target so it's fine at these sizes. The detail that matters is iterating the sums downwards — going upwards would let me use the same element twice and turn it into the unbounded version.”
Solve on LeetCodemediumTarget Sum
Count the ways to assign plus and minus to each number so the total equals a target.
Trigger'Assign signs', 'plus or minus each element' — subset sum in disguise.
Approach- Rewrite the algebra: if P is the positive subset and N the negative, P − N = target and P + N = total. So P = (target + total) / 2.
- That turns it into: how many subsets sum to P? Classic subset sum counting.
- Guard: if (target + total) is odd or negative, the answer is zero.
O(n × sum) time, O(sum) space.
PitfallGoing straight to exponential enumeration of sign assignments without spotting the reduction.
Say it out loud“The signed sum can be rewritten: the positive subset minus the negative one is the target, and together they're the total. Solving those two gives me a single target for the positive subset, so this is subset-sum counting — which is polynomial in the sum instead of exponential in n.”
Solve on LeetCodemediumUnique Paths
Count the lattice paths from the top-left to the bottom-right of a grid, moving only right and down.
Trigger'How many paths in a grid' with restricted moves.
Approach- DP: dp[i][j] = dp[i−1][j] + dp[i][j−1], with the first row and column all ones.
- One row of space is enough.
- Then give the closed form: every path is m−1 downs and n−1 rights, so it is C(m+n−2, m−1).
O(m × n) with DP, or O(min(m,n)) with the combinatorial formula.
PitfallNot spotting that it is a combinatorics problem. The closed form is a free extra point.
Say it out loud“The DP is two lines, but there's a closed form: every path is the same multiset of moves in some order, so it's m plus n minus two choose m minus one. I'd write the DP because it generalises to obstacles, but I'd mention the formula.”
Solve on LeetCodemediumWord Break
Decide whether a string can be segmented into words from a dictionary.
Trigger'Can this be split into valid pieces', 'segment the string'.
Approach- State: dp[i] = can the prefix of length i be segmented?
- Transition: dp[i] is true if some j < i has dp[j] true and s[j:i] in the dictionary.
- Put the dictionary in a set for O(1) lookup.
- Bound the inner loop by the longest word to cut constant factors.
O(n² × L) time, O(n) space.
PitfallGreedy longest-match. 'aaaaab' with words ['aaaa', 'aaa', 'b'] breaks it.
Say it out loud“Greedy longest-match fails — I can consume a prefix that makes the rest unsegmentable. So it's DP over prefixes: position i is reachable if some earlier reachable position is followed by a dictionary word. The dictionary goes in a set so the membership check is constant.”
Solve on LeetCodehardBurst Balloons
Maximise the coins from bursting balloons, where each burst's value depends on its current neighbours.
TriggerAn interval problem where the order of operations changes the values.
Approach- The trap: thinking forwards. Which balloon you burst first changes everything after it.
- Invert it — think about which balloon is burst **last** in a range. Then its neighbours are the range boundaries, which are fixed.
- State: dp[i][j] = the best over the open interval (i, j). Transition: pick the last balloon k.
- Pad the array with ones at both ends to remove the boundary cases.
O(n³) time, O(n²) space.
PitfallDefining the state as 'first balloon burst'. The subproblems then are not independent and the recurrence does not close.
Say it out loud“The obvious framing — which do I burst first — doesn't decompose, because bursting changes the neighbours of everything left. If instead I ask which balloon is burst *last* in a range, its neighbours are exactly the range boundaries, which are fixed. That makes the subproblems independent and gives an interval DP.”
Solve on LeetCode