House Robber
Maximize the sum of chosen array elements with no two adjacent — a take-or-skip 1D DP with two rolling variables.
Overview
Houses along a street hold nums[i] money; robbing two adjacent houses triggers an alarm. Maximize the loot. For [2, 7, 9, 3, 1] the answer is 12 (2 + 9 + 1). The problem is the simplest "take or skip with a constraint" DP and the template for a large family: circular streets (House Robber II), trees (House Robber III, see Tree DP), and "delete and earn".
Its state and iteration are identical to Climbing Stairs, but the combination rule is max over two choices instead of a sum — a good illustration that DP problems differ mainly in the transition, not the skeleton.
Intuition
A mental model before the formal terms.
Stand at the last house. Either you rob it — then the previous house is off-limits and you get nums[i] + (best loot from houses 0..i-2) — or you skip it and keep (best loot from houses 0..i-1). Whichever is bigger is the best loot from houses 0..i.
For [2, 7, 9, 3, 1]: best up to house 0 is 2; up to house 1 is max(2, 7) = 7; up to house 2 is max(7, 9 + 2) = 11; up to house 3 is max(11, 3 + 7) = 11; up to house 4 is max(11, 1 + 11) = 12. Note that greedy "take the biggest, skip its neighbors" also gives 12 here but fails on [2, 1, 1, 2] (greedy: 2 + 1 = 3, optimal 4).
How it works
- State:
dp[i]= maximum money obtainable from houses0..i(inclusive), whether or not houseiis robbed. - Transition:
dp[i] = max(dp[i-1], nums[i] + dp[i-2])— skip housei, or rob it and add the best from two houses back. - Base case:
dp[0] = nums[0],dp[1] = max(nums[0], nums[1]). With a sentineldp[-1] = 0the loop can start ati = 0. - Iteration order:
ifrom left to right. - Answer location:
dp[n-1]. - Space optimization: two rolling variables
prev2 = dp[i-2],prev1 = dp[i-1]. An equivalent two-state formulation keepsrobbed(best ending with houseirobbed) andskipped(best with houseinot robbed):robbed' = skipped + nums[i],skipped' = max(robbed, skipped)— this is the State Machine DP view.
Why it works
Optimal substructure: any valid selection over houses 0..i either includes house i or not. If it does, it cannot include i-1, so the rest is a valid selection over 0..i-2 — and it must be the optimal one, or we could swap in a better one. If it does not include i, it is a valid selection over 0..i-1, again necessarily optimal. So the max of the two cases is exact.
The dp[i] definition deliberately means "best over the prefix" rather than "best ending at i"; that is what lets the skip branch be a plain dp[i-1] without further casework.
Only n states with O(1) work each, versus 2^n subsets in brute force.
Recognition
How to tell a problem wants this.
- "Cannot pick two adjacent / consecutive elements", "choose elements with a gap of at least one".
- Maximize a sum under a local exclusion constraint on a line.
- Circular versions: solve twice on
nums[0..n-2]andnums[1..n-1].
Interactive visualization
Play, step, change the input. ← → and space work too.
| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 2 | 7 | · | · | · | · | · |
1dp[0] = v[0]; dp[1] = max(v[0], v[1])2for i in 2 .. n-1:3 take = dp[i-2] + v[i]4 skip = dp[i-1]5 dp[i] = max(take, skip)6return dp[n-1]Pseudocode
1prev2 = 0, prev1 = 02for x in nums:3 cur = max(prev1, prev2 + x)4 prev2 = prev15 prev1 = cur6return prev1Implementations
1# House robber: maximise the sum of a subset with no two adjacent elements.2# dp[i] = max(dp[i-1], dp[i-2] + a[i]) — skip this house, or take it and3# inherit from two back. Only two previous values matter, so O(1) space.4 5 61 · Rolling two-variable form: prev2 = dp[i-2], prev1 = dp[i-1]7def rob(a: list[int]) -> int:8 prev2 = prev1 = 09 for x in a:102 · Take x (adding dp[i-2]) or skip it (keeping dp[i-1])11 prev2, prev1 = prev1, max(prev2 + x, prev1)12 return prev113 14 153 · Circular street: house 0 and house n-1 are now adjacent16def rob_circular(a: list[int]) -> int:17 n = len(a)18 if n == 0:19 return 020 if n == 1:21 return a[0]22 # Either skip the last house, or skip the first — never both ends together23 return max(rob(a[:-1]), rob(a[1:]))24 25 264 · Recovering which houses were robbed needs the full dp array27def rob_which(a: list[int]) -> list[int]:28 n = len(a)29 if n == 0:30 return []31 dp = [0] * (n + 1)32 dp[1] = a[0]33 for i in range(2, n + 1):34 dp[i] = max(dp[i - 1], dp[i - 2] + a[i - 1])35 365 · Walk back: a house was taken iff dp[i] came from dp[i-2] + a[i-1]37 chosen: list[int] = []38 i = n39 while i > 0:40 if i >= 2 and dp[i] == dp[i - 1]:41 i -= 1 # this house was skipped42 else:43 chosen.append(i - 1)44 i -= 2 # this house was taken, so skip its neighbour45 return chosen[::-1]prev2, prev1 = prev1, max(prev2 + x, prev1)is the simultaneous update in one statement — the right-hand side is fully evaluated before either name is rebound, so no temporaries are needed.- That single line is the clearest expression of the rolling recurrence in any of the four languages, and it removes the ordering bug the other three can make.
a[:-1]anda[1:]build the circular sub-problems; both copy, which is the same cost as the other languages.chosen[::-1]reverses into a new list, matching thelist[int]return type —chosen.reverse()would returnNone.- The reconstruction uses an explicit
whilebecause the step depends on the branch, which aforover arangecannot express.
- Tuple assignment evaluates the whole right-hand side first, which is exactly what a simultaneous DP update needs and what the other three languages must arrange manually.
a[:-1]is the idiomatic "all but the last"; negative slice bounds are a Python (and JS) convenience C++ lacks.chosen[::-1]returns a new list whilelist.reverse()returnsNone— a recurring source of accidentalNonereturns.functools.lru_cacheon a recursive formulation is the memoised alternative, but it uses O(n) stack and hits the recursion limit around n = 1000.
- Writing
return chosen.reverse(), which returnsNone. - Using the recursive memoised form on a large input and hitting
RecursionError. - Initialising
prev1 = a[0]instead of 0, which breaks the empty-list case.
- Simultaneous update: Python tuple assignment expresses
prev2, prev1 = prev1, max(...)directly, while C++ and JS/TS must compute into temporaries first or get the ordering wrong — a real bug the Python form cannot make. - Negative slice indices (
a[:-1],a.slice(0, -1)) exist in Python and JS/TS; C++ needs explicit iterator arithmetic. - Reversal: C++
std::reverseand JS/TSArray.prototype.reversemutate and are chainable; Pythonlist.reverse()returnsNone, so the slice form is the expression. - Recursion is a viable alternative only in C++ and JS/TS at this scale — CPython would hit
RecursionErroron a list of a few thousand houses.
Complexity
Tabulated array version uses O(n) space; the tree variant is O(n) over the tree with two values per node.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Maximize a sum with a "no two adjacent" (or "gap ≥ k", with a window of
krolling values) constraint. - Delete and Earn: bucket values by number, then House Robber over the value axis.
- Circular arrays via two linear runs; trees via post-order with (rob, skip) pairs.
- The constraint is a global count ("pick exactly k") rather than adjacency — that needs a second DP dimension.
- Elements must be contiguous — that is Kadane's Algorithm.
- Adjacency is defined by an arbitrary graph — maximum weight independent set is NP-hard in general.
Alternatives
Common mistakes
- Greedy "take every other house" or "take the largest and skip neighbors" — fails on
[2, 1, 1, 2]. - Defining
dp[i]as "best ending ati" and then forgetting that the answer ismax(dp)rather thandp[n-1]. - House Robber II: forgetting the
n == 1case, where both slices are empty. - Mis-ordering the rolling update so
prev1is overwritten beforeprev2reads it.
Interview patterns
- House Robber I, II (circular), III (binary tree).
- Delete and Earn: transform to House Robber over value counts.
- Maximum sum with no two adjacent in a 2D grid (row-wise then column-wise) — Pizza With 3n Slices style extensions.
- Stock problems with cooldown — same two-state machine with an extra state.
- Coin ChangeIntermediate