Dynamic Programming I: 1D
Define what dp[i] means, write how it depends on smaller i, and fill a table instead of recomputing overlapping subproblems.
- O(2^n) to O(n)Two-branch recursion against a memo or table
- O(A × k)Over an amount A with k choices
- O(n²)Ending at i, scanning every earlier j
Show what to say
“When a problem asks for the number of ways or a minimum or maximum over choices, and my brute-force recursion repeats the same arguments, I use DP. I define dp[i] in words, write the recurrence from the last decision, set the base cases and fill in order. Exponential becomes O(states × choices), and rolling variables often give O(1) space.”
Trade-offs
- Top-down vs bottom-up: top-down is the brute force plus a memo and suits few reachable states; bottom-up has no stack limit and allows O(1) space.
- DP vs greedy: coins 1, 3, 4 for amount 6 break greedy; a counterexample means DP, a proven exchange argument means greedy.
- Table vs rolling variables: if dp[i] reads only the last one or two cells, two variables give O(1) space; otherwise keep the array.
Bring it up before they ask
Say what dp[i] means in words first, size the table n + 1 when the top is index n, and test a memo with has(), not a truthy get(), because a cached 0 is a real answer.
Dynamic programming is recursion that never solves the same question twice. Write each answer down once, build it from smaller answers, and a slow brute force becomes a quick loop.
When to reach for it
Listing every solution is Backtracking. DP is for when Greedy has a counterexample.
Tap the signals you can see in the problem.
Rememberthe number of waysminimum or maximumis it possible?same arguments
- Fixed look-back
Climbing Stairsdp[i]meansanswer for stepi - Ending at
i
Longest Increasing Subsequencedp[i]meansbest answer ending atnums[i] - Over an amount
Coin Changedp[i]meansanswer for totala - Prefix with a cut
Word Breakdp[i]meansanswer fors.slice(0, i)
The core idea: solve each subproblem once
Picture a staircase with a toll on each step.
Read more · 123 words
You climb one or two steps at a time and want the cheapest way to the top. Ask only about the last move: you reached step i from i - 1 or from i - 2. So the cheapest way to i is the cheaper of those two.
Correct, but slow: every call makes two more, repeating the same questions. There are only n + 1 different ones. So write each answer on a sticky note (a memo) and look it up next time. Compare both presets, then try your own costs with Edit input:
Two things make a problem fit:
- Repeated questions: the same small question keeps coming back.
- Built from smaller answers: the best answer for
icomes from best answers for smaller indices.
best(i) = Math.min(best(i - 1) + cost[i - 1], best(i - 2) + cost[i - 2]); // best(0) = best(1) = 0Plain recursion vs memoized recursion: the call tree
- i
- 5
- calls
- 1
best(5) asks for the cheapest way to stand on the top, step 5, with cost = [1, 4, 2, 3, 1]. The last move came from step 4 or step 3, so ask for both.
function best(i) {
if (i <= 1) return 0;
return Math.min(best(i - 1) + cost[i - 1],
best(i - 2) + cost[i - 2]);
}
function bestMemo(i, memo) {
if (i <= 1) return 0;
if (memo.has(i)) return memo.get(i);
const value = Math.min(bestMemo(i - 1, memo) + cost[i - 1],
bestMemo(i - 2, memo) + cost[i - 2]);
memo.set(i, value);
return value;
}All steps as text
- best(5) asks for the cheapest way to stand on the top, step 5, with cost = [1, 4, 2, 3, 1]. The last move came from step 4 or step 3, so ask for both.
- best(4): the last move onto step 4 came from step 3 or from step 2, so ask for both.
- best(3): the last move onto step 3 came from step 2 or from step 1, so ask for both.
- best(2): the last move onto step 2 came from step 1 or from step 0, so ask for both.
- best(1) is a base case: you may start on step 1 for free, so it costs 0.
- best(0) is a base case: you may start on step 0 for free, so it costs 0.
- best(2) = min(0 + 4, 0 + 1) = 1.
- best(1) is a base case: you may start on step 1 for free, so it costs 0.
- best(3) = min(1 + 2, 0 + 4) = 3.
- best(2) again! It was already solved further left, but plain recursion has no memory, so its whole subtree is rebuilt from scratch.
- best(1) is a base case: you may start on step 1 for free, so it costs 0. This call is still wasted work: it only happens because a repeated subtree is being rebuilt.
- best(0) is a base case: you may start on step 0 for free, so it costs 0. This call is still wasted work: it only happens because a repeated subtree is being rebuilt.
- best(2) = min(0 + 4, 0 + 1) = 1.
- best(4) = min(3 + 3, 1 + 2) = 3.
- best(3) again! It was already solved further left, but plain recursion has no memory, so its whole subtree is rebuilt from scratch.
- best(2) again! It was already solved further left, but plain recursion has no memory, so its whole subtree is rebuilt from scratch.
- best(1) is a base case: you may start on step 1 for free, so it costs 0. This call is still wasted work: it only happens because a repeated subtree is being rebuilt.
- best(0) is a base case: you may start on step 0 for free, so it costs 0. This call is still wasted work: it only happens because a repeated subtree is being rebuilt.
- best(2) = min(0 + 4, 0 + 1) = 1.
- best(1) is a base case: you may start on step 1 for free, so it costs 0. This call is still wasted work: it only happens because a repeated subtree is being rebuilt.
- best(3) = min(1 + 2, 0 + 4) = 3.
- best(5) = min(3 + 1, 3 + 3) = 4. That took 15 calls for only 6 different subproblems: the 8 calls marked ✕ recomputed answers that were already known. One more step in the staircase roughly doubles this.
Step by step: the DP recipe
Six questions, in order, turn a brute force into a DP.
Read more · 145 words
Define the state in words
"
dp[i]= the cheapest cost to stand on stepi." Vague here means wrong later.Write the recurrence from the last decision
The last move came from
i - 1ori - 2. Combine withmin,max,+(counting) or||(feasibility).Pin down the base cases
Here
dp[0] = dp[1] = 0. When counting, doing nothing is one way (dp[0] = 1).Choose the evaluation order
Fill each cell after the cells it reads: loop
iupward.Read the answer cell
dp[n],dp[amount], or the best over all cells.Optimise the space
If
dp[i]reads only the last one or two cells, keep two variables.
Play with it
Rob houses for the most loot without hitting two neighbours. Each cell picks rob (loot[i] + dp[i - 2]) or skip (dp[i - 1]). Plan your own haul, then press Run.
Templates
Template 1: fixed look-back, four ways
Same answers, four costs. Write the plain recursion first. The memo, table and rolling variables are mechanical rewrites.
Bottom-up fills dp from the base cases upward, so the cells each step reads are ready:
This is Climbing StairsEasy and House RobberMedium.
// Form 1: plain recursion, straight from the recurrence. Correct, but best(i - 1) and best(i - 2) both
// re-solve the same smaller steps, so the call tree roughly doubles per step: O(2^n). Fine for n = 20, hopeless for 50.
export function minCostRecursive(cost) {
const best = (i) => {
if (i <= 1) return 0; // you may start on step 0 or 1 for free
return Math.min(best(i - 1) + cost[i - 1], best(i - 2) + cost[i - 2]);
};
return best(cost.length); // the top is one past the last step
}Bottom-up: fill dp[] from the base cases up
- n
- 7
State in words: dp[i] = the cheapest total cost to stand on step i. The top is step 7, one past the last step, so dp needs 8 cells.
function minCost(cost) {
const n = cost.length;
const dp = new Array(n + 1).fill(0);
dp[0] = 0; dp[1] = 0; // start on 0 or 1 free
for (let i = 2; i <= n; i++) {
const oneStep = dp[i - 1] + cost[i - 1];
const twoStep = dp[i - 2] + cost[i - 2];
dp[i] = Math.min(oneStep, twoStep);
}
return dp[n];
}All steps as text
- State in words: dp[i] = the cheapest total cost to stand on step i. The top is step 7, one past the last step, so dp needs 8 cells.
- Base cases: you may start on step 0 or step 1 without paying, so dp[0] = 0 and dp[1] = 0. The recurrence can't build these: they have no two earlier steps.
- To stand on step 2, the last move came from step 1 (dp[1] + cost[1] = 0 + 4 = 4) or from step 0 (dp[0] + cost[0] = 0 + 1 = 1). Both cells are already filled.
- dp[2] = min(4, 1) = 1, coming from step 0. Every later cell reads this number instead of recomputing it.
- To stand on step 3, the last move came from step 2 (dp[2] + cost[2] = 1 + 2 = 3) or from step 1 (dp[1] + cost[1] = 0 + 4 = 4). Both cells are already filled.
- dp[3] = min(3, 4) = 3, coming from step 2. Every later cell reads this number instead of recomputing it.
- To stand on step 4, the last move came from step 3 (dp[3] + cost[3] = 3 + 3 = 6) or from step 2 (dp[2] + cost[2] = 1 + 2 = 3). Both cells are already filled.
- dp[4] = min(6, 3) = 3, coming from step 2. Every later cell reads this number instead of recomputing it.
- To stand on step 5, the last move came from step 4 (dp[4] + cost[4] = 3 + 1 = 4) or from step 3 (dp[3] + cost[3] = 3 + 3 = 6). Both cells are already filled.
- dp[5] = min(4, 6) = 4, coming from step 4. Every later cell reads this number instead of recomputing it.
- To stand on step 6, the last move came from step 5 (dp[5] + cost[5] = 4 + 5 = 9) or from step 4 (dp[4] + cost[4] = 3 + 1 = 4). Both cells are already filled.
- dp[6] = min(9, 4) = 4, coming from step 4. Every later cell reads this number instead of recomputing it.
- To stand on step 7, the last move came from step 6 (dp[6] + cost[6] = 4 + 2 = 6) or from step 5 (dp[5] + cost[5] = 4 + 5 = 9). Both cells are already filled.
- dp[7] = min(6, 9) = 6, coming from step 6. Every later cell reads this number instead of recomputing it.
- The answer cell is dp[7] = 6. Following the "came from" choices back from the top gives the path: pay on steps 0, 2, 4, 6. Each cell only read the two before it, so two variables would be enough (O(1) space).
Template 2: DP over an amount
A target number and reusable pieces?
Read more · 52 words
Index the table by amount. dp[a] tries every last piece and reads the cell for what is left. Here, the fewest perfect squares that sum to n:
Swap squares for coins and you have Coin ChangeMedium. Swap min for + with dp[0] = 1 to count ordered ways. Unordered ways need 2D DP.
DP over an amount: fewest perfect squares
- n
- 12
State in words: dp[a] = the fewest perfect squares that add up to a. Every cell starts at ∞, which means "not solved yet".
function numSquares(n) {
const dp = new Array(n + 1).fill(Infinity);
dp[0] = 0;
for (let a = 1; a <= n; a++) {
for (let k = 1; k * k <= a; k++) {
dp[a] = Math.min(dp[a], dp[a - k * k] + 1);
}
}
return dp[n];
}All steps as text
- State in words: dp[a] = the fewest perfect squares that add up to a. Every cell starts at ∞, which means "not solved yet".
- Base case: the amount 0 needs no squares at all, so dp[0] = 0. Every other amount is built on top of it.
- dp[1]: try each square that fits as the last piece. 1 leaves 0: dp[0] + 1 = 1. The fewest is 1.
- dp[2]: try each square that fits as the last piece. 1 leaves 1: dp[1] + 1 = 2. The fewest is 2.
- dp[3]: try each square that fits as the last piece. 1 leaves 2: dp[2] + 1 = 3. The fewest is 3.
- dp[4]: try each square that fits as the last piece. 1 leaves 3: dp[3] + 1 = 4; 4 leaves 0: dp[0] + 1 = 1. The fewest is 1.
- dp[5]: try each square that fits as the last piece. 1 leaves 4: dp[4] + 1 = 2; 4 leaves 1: dp[1] + 1 = 2. The fewest is 2.
- dp[6]: try each square that fits as the last piece. 1 leaves 5: dp[5] + 1 = 3; 4 leaves 2: dp[2] + 1 = 3. The fewest is 3.
- dp[7]: try each square that fits as the last piece. 1 leaves 6: dp[6] + 1 = 4; 4 leaves 3: dp[3] + 1 = 4. The fewest is 4.
- dp[8]: try each square that fits as the last piece. 1 leaves 7: dp[7] + 1 = 5; 4 leaves 4: dp[4] + 1 = 2. The fewest is 2.
- dp[9]: try each square that fits as the last piece. 1 leaves 8: dp[8] + 1 = 3; 4 leaves 5: dp[5] + 1 = 3; 9 leaves 0: dp[0] + 1 = 1. The fewest is 1.
- dp[10]: try each square that fits as the last piece. 1 leaves 9: dp[9] + 1 = 2; 4 leaves 6: dp[6] + 1 = 4; 9 leaves 1: dp[1] + 1 = 2. The fewest is 2.
- dp[11]: try each square that fits as the last piece. 1 leaves 10: dp[10] + 1 = 3; 4 leaves 7: dp[7] + 1 = 5; 9 leaves 2: dp[2] + 1 = 3. The fewest is 3.
- dp[12]: try each square that fits as the last piece. 1 leaves 11: dp[11] + 1 = 4; 4 leaves 8: dp[8] + 1 = 3; 9 leaves 3: dp[3] + 1 = 4. The fewest is 3.
- Walking the chosen squares back from dp[12] gives 4 + 4 + 4, so the answer is 3. Grabbing the biggest square first would need 4 (9 + 1 + 1 + 1): greedy fails here, the table doesn't.
// DP over an amount: dp[a] = the fewest perfect squares that add up to a.
// Each cell tries every possible LAST piece (a square k² that fits) and reads the cell for what is left.
// Swap the squares for a coin list and this is Coin Change; keep Infinity for amounts that can't be made.
export function numSquares(n) {
const dp = new Array(n + 1).fill(Infinity); // Infinity = "not reachable (yet)"
dp[0] = 0; // the amount 0 needs no pieces
for (let a = 1; a <= n; a++) {
for (let k = 1; k * k <= a; k++) {
dp[a] = Math.min(dp[a], dp[a - k * k] + 1);
}
}
return dp[n];
}Template 3: ending at i, looking back at every earlier j
For "best subsequence" problems, let dp[i] be the best answer ending at nums[i]: scan every earlier j and extend the best.
Read more · 50 words
That is O(n²), and the answer is the best cell.
Add 1 instead of nums[i] for Longest Increasing SubsequenceMedium. Maximum SubarrayMedium only looks at i - 1: dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]). Word BreakMedium asks: is some cut j true, with s.slice(j, i) a word?
// "Ending at i": dp[i] = the best answer whose LAST element is nums[i].
// dp[i] looks back at every j < i that may come right before i, so it costs O(n²).
// The answer can end anywhere, so it is the best cell, not dp[n - 1].
// Here: the largest sum of a strictly increasing subsequence.
export function maxSumIncreasing(nums) {
if (nums.length === 0) return 0;
const dp = nums.slice(); // nums[i] on its own is already a valid subsequence ending at i
let best = -Infinity;
for (let i = 0; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + nums[i]); // extend the best chain ending at j
}
best = Math.max(best, dp[i]);
}
return best;
}Try it
// The same problem four ways. Edit cost and press Run: all four must agree.
const cost = [1, 4, 2, 3, 1, 5, 2];
console.log(minCostRecursive(cost), minCostMemo(cost), minCostTable(cost), minCostRolling(cost)); // 6 6 6 6
// Why the memo matters: count the calls each version makes on a 25-step staircase.
const long = Array.from({ length: 25 }, (_, i) => (i * 7) % 10);
let plainCalls = 0;
const plain = (i) => {
plainCalls++;
return i <= 1 ? 0 : Math.min(plain(i - 1) + long[i - 1], plain(i - 2) + long[i - 2]);
};
let memoCalls = 0;
const memo = new Map();
const cached = (i) => {
memoCalls++;
if (i <= 1) return 0;
if (!memo.has(i)) memo.set(i, Math.min(cached(i - 1) + long[i - 1], cached(i - 2) + long[i - 2]));
return memo.get(i);
};
console.log(plain(25), `${plainCalls} calls`); // 41 242785 calls
console.log(cached(25), `${memoCalls} calls`); // 41 49 calls
// DP over an amount: the fewest perfect squares that add up to n.
console.log(numSquares(12), numSquares(13)); // 3 2Complexity
Time is states × work per state.
Read more · 32 words
| Form | Time | Space |
|---|---|---|
| Plain recursion | O(2^n) | O(n) |
| Memo or table | O(n) | O(n) |
| Rolling variables | O(n) | O(1) |
Ending at i, every j | O(n²) | O(n) |
JavaScript you'll use
JavaScript refresher: Recursion, the call stack and memoization Arrays: create, read, update Map and SetRead more · 48 words
The memo trap: if (memo.get(i)) treats a cached 0 as missing, so test memo.has(i). Memoized recursion on n = 10^5 overflows the stack: go bottom-up.
const dp = new Array(n + 1).fill(0); // one cell per state, n + 1 when the top is index n
const best = new Array(amount + 1).fill(Infinity);
best[0] = 0; // Infinity = "can't be made", min() ignores it
const memo = new Map(); // if (memo.has(key)) return memo.get(key);
[twoBack, oneBack] = [oneBack, current]; // roll two variables forward
return best[amount] === Infinity ? -1 : best[amount];Interview questions
Top-down or bottom-up: which should I write in an interview?
Top-down when the recurrence is the hard part: it is the brute force plus a two-line memo. Bottom-up for large inputs or O(1) space.
How do you know it's DP and not greedy?
Try to break greedy on a small case: coins 1, 3, 4 for amount 6. Greedy gives 4 + 1 + 1, but 3 + 3 is better. A counterexample means DP.
The interviewer wants the actual choices, not just the value. What changes?
Keep the full table. Walk back from the answer cell, asking which option produced each cell.
Check yourself
Question 1 of 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Climbing StairsCount the different ways to climb n steps when each move climbs one or two steps.Easy
- Min Cost Climbing StairsFind the cheapest way to climb past the top of a staircase when every step you stand on has a price.Easy
- House RobberTake the most cash from a row of houses without ever taking from two neighbours.Medium
- House Robber IIRob houses arranged in a circle, never two neighbours, for the largest total.Medium
- Maximum SubarrayFind the largest sum of a non-empty run of consecutive numbers (Kadane's algorithm).Medium
- Coin ChangeFind the fewest coins that add up to an amount, or -1 when the amount can't be made.Medium
- Decode WaysCount the ways a string of digits can be read as letters when A = 1 through Z = 26.Medium
- Maximum Product SubarrayFind the contiguous subarray with the largest product.Medium
- Longest Palindromic SubstringReturn the longest contiguous piece of a string that reads the same forwards and backwards.Medium
- Palindromic SubstringsCount how many contiguous substrings of a string are palindromes.Medium
- Word BreakDecide whether a string can be cut into pieces that are all dictionary words.Medium
- Longest Increasing SubsequenceFind the length of the longest strictly increasing subsequence of an array.Medium
- Split Array Largest SumCut an array into k contiguous pieces so that the largest piece sum is as small as possible.Hard
- Jump GameEach value says how far you may jump from that index. Decide whether you can reach the last index.Medium
- Counting BitsFor every number from 0 to n, count its 1 bits, reusing the answers you have already computed.Easy
In real systems
This pattern is not only for coding rounds. Here is where it does the work in a design interview.