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
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:
- A subproblem is a smaller version of the same question. "How many ways to climb 10 stairs?" has the subproblems "… 9 stairs?", "… 8 stairs?", and so on.
- Overlapping subproblems means the same small question is asked again and again by different branches of a plain recursion (a function that calls itself — see D07 · Recursion). That repetition is what makes plain recursion slow, and what DP removes.
- Optimal substructure means the best answer to the big question is built from best answers to smaller questions. The cheapest route to the top floor passes through some floor below it, and the part up to that floor must itself be the cheapest.
- The state is the small set of numbers that names one subproblem, like "i stairs" or "the first i letters of a and the first j letters of b". The table (usually a Dart
List, sometimes a list of lists) holds one answer per state. - The recurrence is the rule that computes one state's answer from smaller states. It always comes from listing the choices for the last move.
- Memoisation (top-down) keeps the recursion but writes each answer in a notebook (a
Map) the first time. Tabulation (bottom-up) fills the table in a loop from the smallest state upwards, with no recursion at all.
How to recognise a DP problem
- 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…").
- 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.
- After a few choices you land in a situation you have seen before by a different route — the subproblems overlap.
- 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.
- 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
- 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. - Write the recurrence from the choices. Ask "what was the last move?" and list every option. Combine them with
+for counts,min/maxfor optimums,orfor yes/no. - 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.
- 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).
- 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?
rows = 3, cols = 410rows = 1, cols = 51rows = 2, cols = 22- 1 ≤ rows, cols ≤ 100
- only grids whose answer is at most 2·109 are given
Run the recipe on it, one step at a time:
- State:
dp[r][c]= the number of different paths from the start to cell (r, c). - 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]. - Base cases: every cell of the top row and the left column has exactly 1 path — a straight line.
- Order: row by row, left to right. Then "above" and "left" are always filled before they are read.
- Space: a cell reads only the row above and the cell to its left, so one row of length
colsis 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:
| Problem | State in words | Table |
|---|---|---|
| Climbing Stairs | ways to stand on step i | 1-D, size n + 1 |
| House Robber | most money from the first i houses | 1-D, size n + 1 |
| Coin Change | fewest coins making amount a | 1-D, size amount + 1 |
| Longest Increasing Subsequence | longest run ending at index i | 1-D, size n |
| Word Break | can the first i letters be split | 1-D, size n + 1 |
| Partition Equal Subset Sum | can some numbers add up to s | 1-D, size total ÷ 2 + 1 |
| Unique Paths | paths to cell (r, c) | 2-D, rows × cols |
| Longest Common Subsequence | LCS of the first i letters of a and first j of b | 2-D, (m + 1) × (n + 1) |
| Edit Distance | edits turning the first i letters of a into the first j of b | 2-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.
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.)
n = 45n = 613n = 11- 1 ≤ n ≤ 45
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:
| n | calls made by plain recursion | cells filled by the table |
|---|---|---|
| 10 | 177 | 11 |
| 20 | 21 891 | 21 |
| 30 | 2 692 537 | 31 |
| 40 | 331 160 281 | 41 |
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.
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?
nums = [6, 1, 2, 9, 3]15nums = [4, 10, 3, 1, 8]18nums = [5]5- 1 ≤ n ≤ 100
- 0 ≤ nums[i] ≤ 400
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.
nums = [9, 4, 2, 8] (a circle)12nums = [5, 3, 4, 11, 2]16nums = [7]7dp[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.
coins = [1, 5, 6, 9], amount = 112coins = [4, 6], amount = 9-1coins = [2], amount = 00- 1 ≤ number of coin values ≤ 12, all different
- 1 ≤ coin ≤ 231 − 1
- 0 ≤ amount ≤ 104
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:
| a | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| dp[a] | 0 | 1 | 2 | 3 | 4 | 1 | 1 | 2 | 3 | 1 | 2 | 2 |
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.
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.
nums = [3, 8, 4, 9, 5, 6]4nums = [6, 2, 7, 3, 4, 8, 1]4nums = [7, 7, 7]1- 1 ≤ n ≤ 2500
- −104 ≤ nums[i] ≤ 104
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.
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.
s = "sunflowerpot", words = [sun, flower, sunflower, pot, flow]trues = "goodbyes", words = [good, bye, goodby]falses = "potpot", words = [pot]true- 1 ≤ |s| ≤ 300
- 1 ≤ number of words ≤ 1000, each 1 to 20 lowercase letters
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.
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).
a = "stone", b = "longest"3a = "dart", b = "drat"3a = "abc", b = ""0- 0 ≤ |a|, |b| ≤ 1000
- lowercase letters
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).
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.
a = "lemon", b = "melon"2a = "flour", b = "floor"1a = "", b = "code"4- 0 ≤ |a|, |b| ≤ 500
- lowercase letters
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.
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.
nums = [3, 1, 5, 9]truenums = [1, 5]falsenums = [2, 3]false- 1 ≤ n ≤ 200
- 1 ≤ nums[i] ≤ 100
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).
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
| Problem | State | Recurrence | Time | Space (optimised) |
|---|---|---|---|---|
| Unique Paths | dp[r][c] paths to (r, c) | above + left | O(rows · cols) | O(cols) |
| Climbing Stairs | dp[i] ways to step i | dp[i−1] + dp[i−2] | O(n) | O(1) |
| House Robber | dp[i] best from first i houses | max(dp[i−1], dp[i−2] + nums[i−1]) | O(n) | O(1) |
| House Robber II | two straight streets | max(without first, without last) | O(n) | O(1) |
| Coin Change | dp[a] fewest coins for a | 1 + min dp[a−c] | O(amount · k) | O(amount) |
| LIS | dp[i] longest run ending at i | 1 + max dp[j], nums[j] < nums[i] | O(n²) or O(n log n) | O(n) |
| Word Break | dp[i] first i letters split? | some j: dp[j] and s[j..i) a word | O(n²) checks | O(n) |
| LCS | dp[i][j] on two prefixes | match: diagonal + 1; else max(above, left) | O(m · n) | O(n) |
| Edit Distance | dp[i][j] edits between prefixes | match: diagonal; else 1 + min(above, left, diagonal) | O(m · n) | O(n) |
| Partition Equal Subset Sum | can[s] some numbers sum to s | can[s] or can[s−x], s from high to low | O(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).