Dynamic Programming Problems

Eight classic interview problems — Climbing Stairs, House Robber (in a row and on a circle), Coin Change, Longest Increasing Subsequence, Word Break, Longest Common Subsequence, Edit Distance and Partition Equal Subset Sum — all solved with the same five-step recipe. For each one you will write the slow recursion first, see why it explodes, turn it into a table, watch that table fill cell by cell on your own input, and then shrink its memory. By the end you will be able to look at a new "how many ways / smallest / largest / is it possible" problem and write the table yourself.

The DP recipe: five steps that solve every problem on this page

A teacher asks you "what is 7 × 8?" twenty times in one lesson. The first time you work it out. The sensible thing is to write 7 × 8 = 56 on a sticky note and, every later time, just read the note. Dynamic programming (DP) is exactly that habit applied to a big problem: break it into smaller questions, answer each small question once, write the answer down in a table, and build bigger answers by reading the table instead of working things out again.

The full theory — where the name comes from, why it works, and two longer case studies — is in C15 · Dynamic Programming. This page is the practical side: how to solve interview problems with it. A few words first, because every problem below uses them:

How to recognise a DP problem

  1. The question asks for a count ("how many ways…"), an optimum ("fewest", "cheapest", "longest", "most money") or a yes/no ("can you…", "is it possible…").
  2. You make a sequence of choices (take or skip, step 1 or 2, which coin next, keep or drop a letter), and trying all of them would be exponential.
  3. After a few choices you land in a situation you have seen before by a different route — the subproblems overlap.
  4. The obvious greedy rule ("always grab the biggest") has a counterexample. When greedy is provably right, it is simpler — that is the subject of C16 · Greedy Algorithms and of the interval problems in Step 13.10 · Intervals & Greedy.
  5. The input sizes fit a table: n up to a few thousand for an n² table, or a sum/amount up to about 104 to 106 for a table indexed by value.

The five-step recipe

  1. Define the state in words. Write one sentence: "dp[i] = the fewest coins that make exactly i". If you cannot say it in words, you cannot fill it.
  2. Write the recurrence from the choices. Ask "what was the last move?" and list every option. Combine them with + for counts, min/max for optimums, or for yes/no.
  3. Set the base cases. The smallest states you can answer without the recurrence — usually "nothing yet": zero stairs, zero houses, the empty string, amount 0.
  4. Choose the order of filling. Every state must be filled after the states it reads. Top-down memoisation finds the order for you through recursion; bottom-up tabulation needs you to pick it (usually small to large).
  5. Optimise the space. Look at which cells the recurrence reads. If it only reads the last row or the last two cells, keep only those.

Warm-up: Unique Paths

The task. A robot stands in the top-left cell of a grid with rows rows and cols columns. Each move goes one cell right or one cell down. How many different paths reach the bottom-right cell?

Example 1
Input: rows = 3, cols = 4
Output: 10
Why: every path is 2 downs and 3 rights in some order; there are 10 such orders.
Example 2
Input: rows = 1, cols = 5
Output: 1
Why: one row means the robot can only go straight right.
Example 3
Input: rows = 2, cols = 2
Output: 2
Why: right then down, or down then right.
Constraints
Speed you need: listing every path is hopeless (a 100 × 100 grid has about 2.3·1058 paths) → fill a table in O(rows · cols) = 104 steps, O(cols) space after optimising.

Run the recipe on it, one step at a time:

  1. State: dp[r][c] = the number of different paths from the start to cell (r, c).
  2. Choices for the last move: the robot entered (r, c) either from the cell above, (r − 1, c), or from the cell to the left, (r, c − 1). A path cannot do both, so the two groups never overlap and we add them: dp[r][c] = dp[r − 1][c] + dp[r][c − 1].
  3. Base cases: every cell of the top row and the left column has exactly 1 path — a straight line.
  4. Order: row by row, left to right. Then "above" and "left" are always filled before they are read.
  5. Space: a cell reads only the row above and the cell to its left, so one row of length cols is enough (the second function below).

The edge case: a single row. There is nothing to add — every cell is a base case.

1-D or 2-D table?

Count how many numbers you need to name one subproblem. One number (a position, an amount, a length) gives a 1-D list. Two numbers (a position in each of two strings, a row and a column) give a 2-D table, a list of lists in Dart. The table on the right of this chart is what each problem on this page uses:

ProblemState in wordsTable
Climbing Stairsways to stand on step i1-D, size n + 1
House Robbermost money from the first i houses1-D, size n + 1
Coin Changefewest coins making amount a1-D, size amount + 1
Longest Increasing Subsequencelongest run ending at index i1-D, size n
Word Breakcan the first i letters be split1-D, size n + 1
Partition Equal Subset Sumcan some numbers add up to s1-D, size total ÷ 2 + 1
Unique Pathspaths to cell (r, c)2-D, rows × cols
Longest Common SubsequenceLCS of the first i letters of a and first j of b2-D, (m + 1) × (n + 1)
Edit Distanceedits turning the first i letters of a into the first j of b2-D, (m + 1) × (n + 1)

The "+ 1" sizes come from a habit worth copying: let index 0 mean "nothing yet" (zero houses, the empty prefix). Then the base case lives in cell 0 and the loop never reads a negative index.

The pattern of this whole page: state in words → choices for the last move → base cases → fill order → shrink the table. Write the plain recursion first (it is the recurrence in code), then turn it into a loop.

Climbing Stairs

The task. A staircase has n steps. From where you stand you may climb 1 step or 2 steps at a time. In how many different orders of moves can you reach the top step? (Order matters: 1 then 2 is different from 2 then 1.)

Example 1
Input: n = 4
Output: 5
Why: 1+1+1+1, 1+1+2, 1+2+1, 2+1+1, 2+2.
Example 2
Input: n = 6
Output: 13
Why: 8 ways end with a 1-step (from step 5) plus 5 ways end with a 2-step (from step 4).
Example 3
Input: n = 1
Output: 1
Why: a single 1-step; the 2-step would overshoot.
Constraints
Speed you need: plain recursion makes about 3.7·109 calls for n = 45 (tens of seconds) → O(n) time with a table, O(1) space with two variables. The answer for n = 45 is 1 836 311 903.
You are on step 10 and wonder how many ways led here. You do not replay every route. You ask two friends: the one on step 9 knows how many ways reach step 9, and the one on step 8 knows how many reach step 8. Every route to 10 came from one of them with one last move, so you add their two answers. Each friend got their number the same way from the two friends below them.

Brute force (plain recursion). The last move onto step n was a 1-step (from n − 1) or a 2-step (from n − 2). So ways(n) = ways(n − 1) + ways(n − 2), with ways(0) = ways(1) = 1. Correct — and terribly slow:

Why it explodes. ways(10) calls ways(9) and ways(8); ways(9) calls ways(8) again, and so on. The number of calls almost doubles with every extra step:

ncalls made by plain recursioncells filled by the table
1017711
2021 89121
302 692 53731
40331 160 28141

The call count is 2 × F(n + 1) − 1, where F is the Fibonacci sequence 1, 1, 2, 3, 5, … — it grows like 1.618n. Growth rates are explained in C03 · Growth of Functions.

Top-down fix (memoisation). Keep the recursion, add a notebook. The first time ways(k) is solved, store it; every later call reads it in O(1). Now each k from 2 to n is solved once: O(n) time, plus O(n) for the notebook and the recursion stack.

Bottom-up (the table). State: dp[i] = ways to stand on step i. Recurrence: dp[i] = dp[i − 1] + dp[i − 2]. Base: dp[0] = 1 (standing still at the bottom is one way — the empty sequence of moves) and dp[1] = 1. Order: i = 2, 3, …, n.

The edge case n = 1 never reaches the table at all: the first line answers it.

Complexity. Table: time O(n), space O(n). Two variables: time O(n), space O(1), because dp[i] reads only the two cells just before it.

Common mistakes. (1) Setting dp[0] = 0: "zero ways to climb zero steps" sounds right, but then dp[2] = dp[1] + dp[0] = 1, losing the single 2-step. The empty sequence of moves is one way. (2) Building List.filled(n + 1, 0) and then writing dp[1] when n = 0 or reading dp[2] when n = 1 — handle tiny n before the loop. (3) Using plain recursion "because it passed the examples": it passes n = 10 and times out at n = 45.

Follow-ups interviewers ask. Each step has a toll and you want the cheapest climb (p11-q3). Hops of 1, 2 or 3 (p11-q4). Tiling a 2 × n board with dominoes is the same recurrence in disguise (p11-q6). Some steps are broken — set their dp to 0 and keep going.

The idea underneath: the recursion tree and its repeated calls — D07 · Recursion — and the "elements of DP" in C15 · Elements of dynamic programming.

House Robber (a street, then a circle)

The task. Houses stand in a row; nums[i] is the money in house i. A burglar alarm rings if two neighbouring houses are robbed on the same night. What is the most money you can collect without ringing it?

Example 1
Input: nums = [6, 1, 2, 9, 3]
Output: 15
Why: rob houses 0 and 3: 6 + 9. Taking every other house (6 + 2 + 3 = 11) is worse.
Example 2
Input: nums = [4, 10, 3, 1, 8]
Output: 18
Why: houses 1 and 4: 10 + 8. The gap between robbed houses can be more than one house.
Example 3
Input: nums = [5]
Output: 5
Why: one house, no neighbours.
Constraints
Speed you need: trying every set of houses is up to 2100 sets → O(n) time, O(1) space.
Walk down the street with a notebook. At each house you write one number: "the most I could have by now". At house i there are only two stories: you robbed it — then you could not have robbed house i − 1, so take your note from two houses back and add this house's money — or you left it — then your note is the same as at the previous house. Write the bigger of the two and move on.

Brute force. At house i, either rob it (and jump to i + 2) or skip it (go to i + 1). Two branches per house: O(2n), and the same "best from house k onwards" is recomputed many times.

The recipe. State: dp[i] = the most money from the first i houses (houses 0 to i − 1). Choices for house i − 1: skip it → dp[i − 1]; rob it → dp[i − 2] + nums[i − 1]. So dp[i] = max(dp[i − 1], dp[i − 2] + nums[i − 1]). Base: dp[0] = 0, dp[1] = nums[0]. Order: left to right. Space: only the last two values are read.

Complexity. Time O(n). Space O(n) for the table, O(1) for the two-variable version.

House Robber II: the houses stand in a circle

Now the street is a ring, so the first and last houses are neighbours too. The whole difference is one rule: house 0 and house n − 1 can never both be robbed. So at least one of them is left alone — and that splits the problem into two straight streets we can already solve: one without the last house, one without the first. The answer is the better of the two.

Example 1
Input: nums = [9, 4, 2, 8] (a circle)
Output: 12
Why: on a straight street 9 + 8 = 17 would win, but houses 0 and 3 touch on the circle. Best is 4 + 8.
Example 2
Input: nums = [5, 3, 4, 11, 2]
Output: 16
Why: 5 + 11 uses houses 0 and 3, which do not touch.
Example 3
Input: nums = [7]
Output: 7
Why: a single house is not its own neighbour.
Common mistakes. (1) Writing dp[i] = dp[i − 2] + nums[i] only, as if you must rob every other house — Example 2 skips two houses in a row. (2) For the circle, running the straight version once and hoping: it happily returns 17 for [9, 4, 2, 8]. (3) For the circle with one house, both "without first" and "without last" are empty and give 0 — answer nums[0] before splitting.

Follow-ups interviewers ask. Return which houses were robbed (walk back through the table: if dp[i] = dp[i − 1] house i − 1 was skipped, otherwise robbed — the animation does this at the end). Houses on a binary tree, where a parent and child are neighbours (keep two numbers per node: best with and without it). Delete-and-earn: picking value v deletes all v − 1 and v + 1, which becomes house robber over the values.

The idea underneath: "take it or leave it" choices, the heart of C15 · Classic interview DP.

Coin Change (and why greedy fails)

The task. You have unlimited coins of each value in coins. Find the fewest coins that add up to exactly amount, or −1 if no combination works.

Example 1
Input: coins = [1, 5, 6, 9], amount = 11
Output: 2
Why: 5 + 6. Grabbing the biggest coin first gives 9 + 1 + 1 = 3 coins.
Example 2
Input: coins = [4, 6], amount = 9
Output: -1
Why: 4 and 6 are even, so every total they make is even; 9 is odd.
Example 3
Input: coins = [2], amount = 0
Output: 0
Why: zero coins already make 0.
Constraints
Speed you need: trying every handful of coins is exponential in the amount → O(amount · coins) = 1.2·105 steps, O(amount) space.
A shopkeeper prepares a cheat sheet before the shop opens: for every price from 1 up to the largest price, the fewest coins that pay it. To fill the row for price 7, she does not start from scratch. She asks: "if the last coin I hand over is a 1, the rest is 6 — my sheet already says how many coins 6 needs. If it is a 5, the rest is 2. If it is a 6, the rest is 1." She picks the cheapest of those and adds one coin.

Brute force. Try every coin as the last coin and recurse on what is left. The same remaining amount is reached by many routes (5 then 1 leaves the same rest as 1 then 5), so the work is exponential.

Adding a notebook gives the top-down version — each amount is solved once:

The recipe. State: dp[a] = the fewest coins that make exactly a. Choices: the last coin is some c ≤ a, leaving a − c. So dp[a] = 1 + min over coins c ≤ a of dp[a − c]. Base: dp[0] = 0; every other cell starts at ∞ ("cannot be made yet"). Order: a = 1, 2, …, amount — every a − c is smaller than a, so it is already final.

The finished table for Example 1, coins [1, 5, 6, 9] and amount 11:

a01234567891011
dp[a]012341123122

The edge case: an amount no combination can make. The ∞ in the last cell survives the whole loop.

Why "biggest coin first" is wrong

The greedy rule — keep handing over the biggest coin that still fits — works for everyday coin systems like 1, 2, 5, 10, 20, 50. It fails for others. With coins 1, 3, 4 and amount 6, greedy takes 4, then 1, then 1 (three coins), but 3 + 3 uses two. Greedy commits to the 4 and can never take it back; the table keeps every option open until the end. Greedy can even get stuck with coins 3 and 5 and amount 9: it takes 5, then 3, and 1 is left over, although 3 + 3 + 3 works. The proof-level story is in C16 · Coin changing.

Complexity. Time O(amount · k) for k coin values; space O(amount). There is no further space saving: dp[a] can read a cell as far back as the largest coin.

Common mistakes. (1) Starting the table at 0 instead of ∞: every min then stays 0 (p11-q11 is this bug). (2) Using double.infinity (a double, which cannot go into a List<int>) or the largest int 9223372036854775807 as ∞ — adding 1 to it wraps around to a negative number in native Dart, and that negative number then wins every min. A large but safe value such as 1 << 30 is enough because no answer exceeds the amount. (3) Forgetting c ≤ a and reading dp[-3]. (4) Returning ∞ instead of −1.

Follow-ups interviewers ask. Count the number of ways to make the amount, where 1 + 2 and 2 + 1 are the same way (p11-q9 — the loop order matters). Fewest perfect squares adding to n (p11-q22 — coin change with square coins). Each coin may be used at most once (that is 0/1 knapsack — see Partition below). Print the coins used (keep the coin that won each cell, as the animation does).

The idea underneath: unbounded choices over a value-indexed table; compare with the 0/1 knapsack in C16 · Fractional vs 0/1 knapsack.

Longest Increasing Subsequence

The task. A subsequence is what is left after deleting some numbers from a list without changing the order of the rest (the kept numbers need not be next to each other). Return the length of the longest subsequence whose numbers strictly increase.

Example 1
Input: nums = [3, 8, 4, 9, 5, 6]
Output: 4
Why: 3, 4, 5, 6. The run 3, 8, 9 is only length 3.
Example 2
Input: nums = [6, 2, 7, 3, 4, 8, 1]
Output: 4
Why: 2, 3, 4, 8.
Example 3
Input: nums = [7, 7, 7]
Output: 1
Why: "strictly" increasing: equal numbers cannot follow each other.
Constraints
Speed you need: trying every subsequence is 22500 → the O(n²) table is 6.25·106 steps (fine); with n up to 105 you would need the O(n log n) version.
People stand in a queue holding numbers. Each person works out "the longest increasing chain that ends with me". To do that they look back at everyone in front holding a smaller number, take the longest chain any of them reported, and add themselves. Nobody needs to look at the people behind them.

Brute force. For each number, take it (if it is bigger than the last one taken) or skip it: 2n subsequences.

The recipe. The trick is the state: "the longest run in the first i numbers" is hard to extend, because you do not know what that run ends with. So pin down the end. State: dp[i] = the length of the longest increasing subsequence that ends at index i. Choices: the number before nums[i] in that run is some earlier nums[j] < nums[i], or there is none. So dp[i] = 1 + max(dp[j]) over those j, or 1. Base: every dp[i] starts at 1 (the number alone). The answer is the largest dp[i], not dp[n − 1].

The O(n log n) version: patience piles

Deal the numbers like cards into piles from left to right; each card goes on the leftmost pile whose top is ≥ the card, or starts a new pile on the right. The number of piles is the answer. In code, keep only the pile tops in a list tails: tails[k] = the smallest last number of any increasing subsequence of length k + 1 seen so far. Small ends are good — they leave more room for later numbers to extend the run. tails is always sorted, so each new number finds its place by binary search (Step 13.5 · Binary Search): if it is bigger than every tail it makes the longest run one longer; otherwise it replaces the first tail that is ≥ it.

Complexity. Table version: time O(n²) (every pair j < i), space O(n). Patience version: time O(n log n) (one binary search per number), space O(n). The table has no easy space saving, because dp[i] reads every earlier cell.

Common mistakes. (1) Returning dp[n − 1]: the longest run need not end at the last number (Example 2's ends at 8, not at 1). (2) Using ≤ instead of <, which counts [7, 7, 7] as length 3. In the patience version that bug hides in the binary search: search for the first tail ≥ x, not > x. (3) Believing tails is the actual subsequence: for [6, 2, 7, 3, 4, 8, 1] it ends as [1, 3, 4, 8], but 1 comes after 8 in the input. Only its length is meaningful (p11-q34).

Follow-ups interviewers ask. Print one longest subsequence (remember, for each i, which j it extended — the animation does this). Count how many longest subsequences there are (keep a count next to each dp[i]). Nesting picture frames or envelopes by width and height (p11-q26 — sort, then LIS on the heights).

The idea underneath: choosing a state that makes the recurrence possible, discussed in C15 · Elements of dynamic programming; binary search on a sorted list in Step 13.5 · Binary Search.

Word Break

The task. Given a string s with no spaces and a list of dictionary words, answer true if spaces can be inserted into s so that every piece is a dictionary word. A word may be used any number of times.

Example 1
Input: s = "sunflowerpot", words = [sun, flower, sunflower, pot, flow]
Output: true
Why: "sun flower pot" (and also "sunflower pot").
Example 2
Input: s = "goodbyes", words = [good, bye, goodby]
Output: false
Why: "good bye" and "goodby" both leave a lonely "s" or "es" that is not a word.
Example 3
Input: s = "potpot", words = [pot]
Output: true
Why: the same word can be reused.
Constraints
Speed you need: trying every way to cut s is 2299 → the table checks n²/2 = 4.5·104 cut points, each a substring of up to n letters, about O(n³) = 2.7·107 character steps in the worst case — fine.
You are reading a sign with the spaces rubbed off. Put a pencil mark under every position where a sentence could have ended so far. The very start gets a mark (zero letters is fine). Then for each later position, look back at the marks: if some mark is followed by a real word that ends exactly here, mark this position too. If the last position gets a mark, the whole sign can be read.

Brute force. Peel any dictionary word off the front and solve the rest. The same leftover suffix is reached through different first words, so the work can be exponential.

The recipe. State: dp[i] = true when the first i letters s[0 .. i) can be cut into words (the notation s[j .. i) means letters j up to but not including i, exactly what Dart's s.substring(j, i) returns). Choices: the last word is s[j .. i) for some cut point j. So dp[i] = true if some j has dp[j] true and s[j .. i) in the dictionary. Base: dp[0] = true. Order: i = 1 … n. Put the words in a Set first so each lookup is O(1) on average.

Complexity. Time O(n²) cut checks, each building a substring of up to n letters, so O(n³) character work in the worst case; if no word is longer than L letters, only try j ≥ i − L and it drops to O(n · L²). Space O(n) for the table plus the dictionary set.

Common mistakes. (1) Greedy longest match: for "abcd" with words [ab, abc, cd], grabbing the longest first word "abc" leaves "d" and answers false, although "ab cd" works. The table tries every cut. (2) Checking the dictionary with words.contains on a List: O(number of words) per check. Convert to a Set. (3) Forgetting dp[0] = true: nothing can ever become true.

Follow-ups interviewers ask. Return every possible sentence, not just yes/no (p11-q25 — memoise the list of sentences per start index). Count the sentences. Use a trie (a letter tree) to walk forward from each cut point instead of building substrings — the subject of Step 13.8 · Tries & Heaps.

The idea underneath: a prefix table over a string; string slicing costs are in D24 · Slice, clean, replace and split.

Longest Common Subsequence

The task. Given two words a and b, return the length of the longest sequence of letters that is a subsequence of both (same order, gaps allowed).

Example 1
Input: a = "stone", b = "longest"
Output: 3
Why: "one" appears in order in both: stone and longest.
Example 2
Input: a = "dart", b = "drat"
Output: 3
Why: "dat" (or "drt"); all four letters cannot keep their order in both.
Example 3
Input: a = "abc", b = ""
Output: 0
Why: nothing is common with an empty word.
Constraints
Speed you need: trying every subsequence of a is 21000 → a table of O(|a| · |b|) = 106 cells, O(|b|) space with two rows.
Two friends each read out a list of the songs they heard this week, in order. You want the longest playlist that appears, in order, in both lists. Look at the last song of each list. If it is the same song, it can end the playlist — keep it and compare the rest. If not, at least one of those two songs is not in the playlist, so try dropping one or the other and keep the better result.

Brute force. Compare first letters: equal → keep both and move on; different → drop the first letter of a, or of b, and take the better. Two branches per mismatch: up to 2m + n calls, with the same pair of suffixes solved over and over.

The recipe. Two words, so the state needs two numbers. State: dp[i][j] = the LCS length of the first i letters of a and the first j letters of b. Choices on the last letters a[i − 1] and b[j − 1]: equal → dp[i − 1][j − 1] + 1 (diagonal); different → max(dp[i − 1][j], dp[i][j − 1]) (above or left). Base: row 0 and column 0 are 0 (an empty prefix has nothing in common). Order: row by row, left to right. In the animation, the column marked ∅ means "no letters of b yet" and the row marked ∅ means "no letters of a yet".

Complexity. Time O(m · n), space O(m · n) for the table. Space optimisation: row i reads only row i − 1, so two rows of length n + 1 are enough — O(n) — but then you can no longer walk back to print the letters (p11-q32).

Common mistakes. (1) Mixing up indices: dp[i][j] talks about a[i − 1] and b[j − 1], because row 0 is the empty prefix. (2) On a match, writing max(above, left) + 1 instead of diagonal + 1: that can count the same letter twice. (3) Confusing subsequence with substring: for "contiguous" the run resets to 0 on a mismatch (p11-q20).

Follow-ups interviewers ask. Print one LCS (p11-q21). Longest common substring (p11-q20). Shortest word that contains both as subsequences: |a| + |b| − LCS. Fewest deletions to make two words equal: |a| + |b| − 2 · LCS. Longest palindromic subsequence: the LCS of a word and its reverse (p11-q29).

The idea underneath: a 2-D table over two prefixes, built step by step in C15 · Longest common subsequence.

Edit Distance

The task. Turn word a into word b using three kinds of edit, each costing 1: insert a letter, delete a letter, or replace a letter with another. Return the fewest edits needed.

Example 1
Input: a = "lemon", b = "melon"
Output: 2
Why: replace the l by m and the m by l; one edit cannot fix two wrong positions.
Example 2
Input: a = "flour", b = "floor"
Output: 1
Why: replace the u by o.
Example 3
Input: a = "", b = "code"
Output: 4
Why: from nothing, insert all four letters.
Constraints
Speed you need: three branches per mismatch is up to 3m + n → a table of O(m · n) = 2.5·105 cells, O(n) space with two rows.
A spell-checker compares the typed word with a dictionary word, letter by letter from the end. If the last letters agree, they cost nothing — compare what is left. If not, there are exactly three ways the last letter got fixed: it was deleted from the typed word, a missing letter was inserted, or the wrong letter was replaced. Try all three on the shorter words and keep the cheapest.

Brute force. The same three choices, as recursion from the front: up to three calls per mismatch, exponential.

The recipe. State: dp[i][j] = the fewest edits turning the first i letters of a into the first j letters of b. Choices on a[i − 1] and b[j − 1]: equal → dp[i − 1][j − 1] for free; otherwise 1 + the smallest of delete dp[i − 1][j] (above), insert dp[i][j − 1] (left), replace dp[i − 1][j − 1] (diagonal). Base: dp[i][0] = i (delete everything) and dp[0][j] = j (insert everything). Order: row by row.

Complexity. Time O(m · n), space O(m · n); with two rows (the second function) space is O(n). Swap the words first so the shorter one indexes the columns, and the rows are as short as possible.

Common mistakes. (1) Forgetting the base row and column — leaving them 0 claims that turning "abc" into "" is free. (2) Adding 1 even when the letters match. (3) Mixing up which neighbour means which edit. It does not change the number, but it matters when you print the edits: above = delete from a, left = insert into a, diagonal = replace (or free match).

Follow-ups interviewers ask. Print the edits (walk back from the corner, as the animation does at the end). Only insert and delete allowed: m + n − 2 · LCS. "One edit away?" in O(n) without a table — walk both words with two pointers. Different costs per edit — the same table with weighted choices.

The idea underneath: the same two-prefix table as LCS with three choices instead of two — see C15 · Classic interview DP.

Partition Equal Subset Sum

The task. Given a list of positive whole numbers, can you split them into two piles with equal sums? Every number goes into exactly one pile.

Example 1
Input: nums = [3, 1, 5, 9]
Output: true
Why: 3 + 1 + 5 = 9 and 9 = 9.
Example 2
Input: nums = [1, 5]
Output: false
Why: the total 6 is even, but no pile can make 3.
Example 3
Input: nums = [2, 3]
Output: false
Why: the total 5 is odd, so two equal whole piles are impossible.
Constraints
Speed you need: trying every pile is 2200 → the total is at most 2·104, so a table indexed by sum costs O(n · total) = 2·106 steps, O(total) space.
Two children want to share a bag of marbles of different weights fairly. That is the same as asking: can one child's share weigh exactly half the total? Keep a row of boxes labelled 0, 1, 2, …, half. Tick a box when you know some marbles weigh exactly that much. Box 0 starts ticked (no marbles). Each new marble of weight x ticks box s whenever box s − x was already ticked before this marble arrived.

Brute force. Put each number on the left or the right pile: 2n ways.

The recipe. First the shortcut: if the total is odd, answer false. Otherwise the question is "can some numbers add up to target = total ÷ 2?" — a 0/1 knapsack (each item taken at most once) on sums. State: can[s] = true when some of the numbers seen so far add up to exactly s. Choices for a new number x: leave it out (can[s] stays) or put it in (can[s − x] from before x). Base: can[0] = true. Order: numbers one by one; for each number, sums from target down to x.

Why backwards? A 2-D table can[k][s] ("using the first k numbers") would read row k − 1 when filling row k. We keep one row and overwrite it in place. Walking down, when we read can[s − x] it is a smaller sum that has not been visited yet in this round — still the old row's value, from before x. Walking up, can[s − x] may have been ticked a moment ago by this same x, so x gets used twice. The third animation below shows exactly that going wrong.

The same input, filled forwards — the classic bug. Watch the single 1 get used three times:

Complexity. Time O(n · target), space O(target) — the space optimisation from a 2-D (n + 1) × (target + 1) table to one row is exactly what the backwards loop makes safe. This is called pseudo-polynomial: fast when the numbers are small, slow when they are huge (a target of 1012 would need 1012 boxes).

Common mistakes. (1) Looping s upwards (p11-q12). Upwards is correct for unlimited copies — that is coin change — and wrong here. (2) Forgetting the odd-total shortcut and building a table for total ~/ 2 anyway: for [2, 3] the target would be 2 and the answer wrongly true. (3) Sorting first and trying greedy: "put each number on the lighter pile" fails for [3, 3, 2, 2, 2].

Follow-ups interviewers ask. Count the ways to give each number a + or − sign so the total is a target (p11-q17). Split into k equal groups (p11-q33 — bitmask DP). Smallest possible difference between the two piles (the largest ticked box ≤ half). Classic 0/1 knapsack with weights and values: the same backwards loop with max instead of or.

The idea underneath: 0/1 knapsack and why it is not greedy — C16 · Fractional vs 0/1 knapsack; the bit tricks for the k-group version in D03 · Numbers & Bits.

Quiz

Interview questions

Variations and follow-ups of the problems above — the questions interviewers move to once the classic version is solved. Every solution is tested in Dart on its examples and on hundreds of random inputs against a slow recursive or try-everything reference.

Cheat sheet

ProblemStateRecurrenceTimeSpace (optimised)
Unique Pathsdp[r][c] paths to (r, c)above + leftO(rows · cols)O(cols)
Climbing Stairsdp[i] ways to step idp[i−1] + dp[i−2]O(n)O(1)
House Robberdp[i] best from first i housesmax(dp[i−1], dp[i−2] + nums[i−1])O(n)O(1)
House Robber IItwo straight streetsmax(without first, without last)O(n)O(1)
Coin Changedp[a] fewest coins for a1 + min dp[a−c]O(amount · k)O(amount)
LISdp[i] longest run ending at i1 + max dp[j], nums[j] < nums[i]O(n²) or O(n log n)O(n)
Word Breakdp[i] first i letters split?some j: dp[j] and s[j..i) a wordO(n²) checksO(n)
LCSdp[i][j] on two prefixesmatch: diagonal + 1; else max(above, left)O(m · n)O(n)
Edit Distancedp[i][j] edits between prefixesmatch: diagonal; else 1 + min(above, left, diagonal)O(m · n)O(n)
Partition Equal Subset Sumcan[s] some numbers sum to scan[s] or can[s−x], s from high to lowO(n · total)O(total)

Loop direction rule: a 1-D table filled from small to large lets an item be used again in the same round (unlimited copies, as in coin change); filling from large to small uses each item at most once (0/1 knapsack, as in partition).