Watch the video version
The same story in nine minutes, with the street, the alarm, the tree and the cart in motion. The written version below goes slower and keeps the code in one place.
Watch on YouTube1 · The problemThe street and the rule
A street of five houses. Every house has cash inside: $2, $7, $9, $8 and $1, in that order. Every pair of neighbours is wired together, and if you rob two houses right next to each other, the alarm goes off.
You may rob as many houses as you like, as long as no two of them are neighbours. The goal: walk away with as much money as possible.
Nothing about this looks hard. It is five numbers. That is exactly why it is a good place to meet dynamic programming: the problem is small enough to hold in your head, and the obvious ideas still fail.
2 · The obvious ideaTake the biggest house
The first instinct is the natural one: go for the richest house. The $9 house gives nine dollars, so take it.
But the moment you do, both of its neighbours are off limits. The $7 and the $8 next door are gone. Of what is left, the biggest is the $2, then the $1.
9 + 2 + 1 = $12
Now look at the plan that skips the $9 entirely: take the $7 and the $8. They are not neighbours — the $9 sits between them — so the alarm stays quiet, and the total is $15.
This is the problem with "always take the biggest": a local choice has consequences two houses away, and the choice itself knows nothing about them. The approach is called greedy, and here it is simply wrong.
3 · Brute forceFine, try everything
If you can't make the right choice immediately, try every choice. At every house there are two options: rob it or skip it. Make both choices at the first house, both again at the second, and so on. Every full sequence of rob and skip decisions is one possible plan.
With five houses that is 2 × 2 × 2 × 2 × 2 = 32 sequences. Throw away the ones that rob two neighbours and 13 legal plans remain. You could check all of them by hand and find the $15.
Now imagine a street of a hundred houses. The tree does not grow a little; it explodes. 2100 is a number with 31 digits:
1 267 650 600 228 229 401 496 703 205 376
The tree is honest, at least: somewhere inside it is the best plan. The problem is that we are solving the same little problem again and again, in different branches, without noticing.
4 · The observationTwo paths, one house
Look at two different branches of that tree. One robs the first house and skips the next two: it reaches the fourth house carrying $2. Another skips the first, robs the second, skips the third: it reaches the same fourth house carrying $7.
Now ask the question that cracks the problem open: standing at this house, do I need to remember every decision that got me here?
No. From this house onward, both histories face exactly the same street and exactly the same rule. Whatever the best thing to do next is, it is the same for both of them. The only thing that matters is how much money each one brought. One brought $7, the other brought $2 — so the $2 history can be thrown away. Not because it was wrong, but because the $7 history can do everything it can do, with more money.
The exact path that produced the $2 no longer matters. At every position, keep only the best result found so far. Everything else about the past is noise.
5 · The stateThe question changes
This changes what we are asking. Instead of "which houses did I rob?" — a question about a whole history — we ask a question about a position:
What is the most money I can have after considering this house?
We will store that answer for every house. Call it dp[i]: the most money you can have after considering the first i + 1 houses (house 0 through house i, counting from zero as the code will). Instead of remembering an enormous tree, we keep one useful number at each position.
That number — the one piece of information you need about the past — is what dynamic programming calls the state. Finding it is the whole game. The rest of this guide is what happens once you have it.
6 · Building itHouse by house
The first house is easy. There is $2 inside and nothing before it, so the best you can have after it is $2. dp[0] = 2.
The second house gives you a choice. Skip it and keep what you had: $2. Or rob it: $7 — but then the first house must be left alone, so $7 is all you get. Seven wins. dp[1] = 7.
The third house, the $9, has the same two choices, and now the second one has a twist. Skip it and keep the best so far: $7. Or rob it — but robbing it means the $7 next door is forbidden, so you cannot add $9 to $7. What can you add it to? The best total from two houses back, where nothing conflicts: $2. So robbing gives 2 + 9 = $11. Eleven beats seven. dp[2] = 11.
Notice that nothing about the third house needed the tree. It needed two numbers from the boxes before it, and one weighing.
7 · The recurrenceThe rule that repeats
By the third house the pattern is already clear, and it is the same at every house after it:
- If I skip this house, my best total is whatever it was at the previous house:
dp[i - 1]. - If I rob this house, the previous house is off limits, so I take the best total from two houses back and add this house's cash:
dp[i - 2] + money[i]. - Whichever is bigger is the answer for this house.
dp[i] = max( dp[i - 1], dp[i - 2] + money[i] )
This one line is the whole idea. The blue term is "skip"; the green term is "rob". A rule like this, that defines an answer in terms of smaller answers of the same kind, is called a recurrence. You did not look it up. You found it by standing at the third house and asking what you were allowed to add the $9 to.
8 · The runWalking the whole street
Apply the rule from the first house to the last. Every row below is one weighing.
| House | Cash | Skip: keep dp[i−1] | Rob: dp[i−2] + cash | Winner | dp[i] |
|---|---|---|---|---|---|
| 1 | $2 | 0 | 0 + 2 = 2 | rob | 2 |
| 2 | $7 | 2 | 0 + 7 = 7 | rob | 7 |
| 3 | $9 | 7 | 2 + 9 = 11 | rob | 11 |
| 4 | $8 | 11 | 7 + 8 = 15 | rob | 15 |
| 5 | $1 | 15 | 11 + 1 = 12 | skip | 15 |
The last box holds the answer: $15, the most money anyone can take from this street without ringing the alarm. Five weighings instead of 32 plans.
Which houses, though?
The boxes tell you how much, and they also tell you which houses, if you read them backwards. Start at the last box. At house 5, dp[4] equals dp[3], so robbing house 5 did not help: it was skipped. At house 4, dp[3] is bigger than dp[2], so house 4 was robbed — jump back two, past its forbidden neighbour, to house 2. There dp[1] is bigger than dp[0]: house 2 was robbed too. The plan is houses 2 and 4: $7 + $8. Exactly the pair the greedy grab locked out.
Every one of the 13 legal plans is still accounted for. Each weighing kept the better of two totals, and a total that lost at some house could never have beaten the winner later — the future was identical for both. The tree was not ignored; it was compressed, one number per house.
9 · The nameThis is dynamic programming
Only now does the idea deserve its name. Dynamic programming means: solve smaller versions of the problem, remember their answers, and use them to build the next answer instead of solving the same thing again.
Here the smaller problems were literally shorter streets: the best total for the first house, then the first two, then the first three. Each answer was remembered in a box, and each new box was built from the two before it. "Programming" in the name has nothing to do with code — it is an old word for planning in a table. The table of boxes is the program.
The thing to carry away is not the word. It is the move that made it possible: realizing that an enormous number of histories could be summarized by a small amount of information, and that once you knew the best result so far, you did not need to remember how you got there.
10 · The codeTranslate what we did into Python
The code is a transcript of the walk. One slot for the answer at every house, the first two filled by hand, and the weighing in a loop for the rest.
def rob(nums): if not nums: return 0 if len(nums) == 1: return nums[0] dp = [0] * len(nums) dp[0] = nums[0] dp[1] = max(nums[0], nums[1]) for i in range(2, len(nums)): dp[i] = max( dp[i - 1], dp[i - 2] + nums[i] ) return dp[-1]
Reading it against the story:
numsis the street: the cash in each house, in order. Our street is[2, 7, 9, 8, 1].- The two
ifs handle streets too short to have a "two houses back": no houses means $0, one house means take it. dp = [0] * len(nums)is the row of empty boxes.dp[0] = nums[0]is the first house: its money is the best you can have.dp[1] = max(nums[0], nums[1])is the second: the larger of the first two, because you cannot have both.- From the third house on, every house follows the same rule, so a loop does the rest. The highlighted line is the recurrence, word for word:
dp[i - 1]is skipping,dp[i - 2] + nums[i]is robbing,maxis the weighing. dp[-1], the last box, is the answer.
For our street the array ends up as [2, 7, 11, 15, 15] — the same five boxes as the table, because it is the same five weighings. Do not think of this array as a collection of numbers that happened to come out of a formula. Every entry has a meaning: the most money I can have after considering this house.
How much work is this?
One pass over the street, one weighing per house: the time grows in proportion to the number of houses, O(n). A hundred houses is a hundred weighings, not a 31-digit number of plans. The array of boxes is also one slot per house, so the extra memory is O(n) as well. That second number is about to shrink.
11 · Less memoryOnly two values
Look at the formula once more. To fill the box at house i, which boxes did we read? Only dp[i - 1] and dp[i - 2]. Never the whole array. Never a box three houses back. So why store all of them?
Keep just the two answers the next house actually needs. Call them prev1 (the previous house's best) and prev2 (the one before that). At every house, compute the current best from those two, then shift them forward: the old prev1 becomes prev2, the current answer becomes prev1.
def rob(nums): prev2 = 0 prev1 = 0 for money in nums: current = max( prev1, prev2 + money ) prev2 = prev1 prev1 = current return prev1
Read it the same way: prev1 is my best if I skip this house, prev2 + money is my best if I rob it, current is the larger, and then the cart rolls forward. Starting both values at 0 even makes the short-street cases disappear: with no houses the loop never runs and the answer is 0; with one house, max(0, 0 + money) is its money.
| House | Cash | prev2 | prev1 | current = max(prev1, prev2 + cash) |
|---|---|---|---|---|
| 1 | $2 | 0 | 0 | max(0, 0 + 2) = 2 |
| 2 | $7 | 0 | 2 | max(2, 0 + 7) = 7 |
| 3 | $9 | 2 | 7 | max(7, 2 + 9) = 11 |
| 4 | $8 | 7 | 11 | max(11, 7 + 8) = 15 |
| 5 | $1 | 11 | 15 | max(15, 11 + 1) = 15 |
Same answers, same order, and the memory no longer grows with the street: two variables whatever its length, O(1) extra space. The time is still one weighing per house, O(n).
The two-variable version forgets the boxes, so it can no longer tell you which houses to rob — only how much. When you need the plan itself, keep the array. Knowing what each version remembers is part of understanding it.
12 · The lessonThree questions for any problem like this
The important part of this problem is not the formula. Formulas are what you end up with; they are not where you start. The important part is the move in section 4: the huge number of possible histories could be summarized by a small amount of information. Once you knew the best result so far, you did not need to remember every way you got there.
So when you meet a new problem that smells like this one — choices, a constraint between them, a best total — do not reach for a recurrence. Ask three things, in order:
- What is my state?
What is the smallest description of "where I am" that still lets me decide what to do next? Here: the house I am considering, and the best total possible after it.
- What choices can I make?
At that state, what are my options? Here: rob this house, or skip it.
- Which smaller states do I need to decide?
Which earlier answers does each choice depend on? Here: skipping needs the previous house's answer; robbing needs the one from two houses back.
Answer those three and the recurrence is not something you remember, it is something you read off your own answers. The code, as you saw, is mostly translating that reasoning line by line.
Try the three questions on the street yourself, from memory, without looking up. If you can say what the state is in one sentence, you have the hard part. Then write the code.
13 · Try itTry your own street
Change the cash, watch the boxes
Type any amounts. The greedy grab and the dynamic-programming walk are both computed live — see how often "take the biggest" loses, and read the plan back from the boxes.
14 · PracticeFour problems, same shape
None of these say "dynamic programming" on the tin. For each, answer the three questions before you write anything. If your state is right, the code is short.
- Warm-upClimbing stairs
A staircase of n steps; you climb 1 or 2 at a time. How many distinct ways to reach the top? The state is "which step am I on"; the two choices are the step sizes. Notice how the previous-states answer rhymes with the street.
- PracticeThe house robber, from scratch
Close this page and write both versions — the array and the two variables — for a street given as a list of amounts. Then test them on a street where greedy wins, and one where it loses.
- VariationMin-cost climbing stairs
Every step has a toll you pay to stand on it; you may start on step 0 or step 1 and climb 1 or 2 at a time. What is the cheapest way to the top? Same state, same choices — but now you are minimizing, and the "cash" is a cost.
- ChallengeDelete and earn
From a list of numbers, pick one, earn its value, and every copy of its neighbours in value (one less, one more) is deleted; repeat. Maximize what you earn. It looks nothing like a street. Find the street inside it.
Don't memorize the table. Learn how to discover it.