Algorithm

House Robber

Dynamic Programming Pattern

House Robber

You are given an integer array nums where nums[i] is the amount of money in the i-th house along a single street. You may rob any set of houses you like, with one restriction: you may never rob two houses that are directly next to each other, because their alarms are linked. Return the largest total amount you can rob. Robbing nothing is allowed, so the answer is never negative.

CONSTRAINTS
  • 1 ≤ nums.length ≤ 100
  • 0 ≤ nums[i] ≤ 400
  • Chosen houses must not be adjacent in the array
  • The houses form a straight line, not a circle
EXAMPLE 1
Input: nums = [1, 2, 3, 1]
Output: 4
Houses 0 and 2 give 1 + 3 = 4. They are two apart, so no alarm links them. Taking the visibly large 3 together with the 2 beside it is forbidden.
EXAMPLE 2
Input: nums = [2, 7, 9, 3, 1]
Output: 12
Houses 0, 2 and 4 give 2 + 9 + 1 = 12. Skipping the 3 at index 3 is what makes room for the 1 at index 4.
EXAMPLE 3
Input: nums = [5, 20, 6, 20, 5]
Output: 40
Both 20s are taken, since indices 1 and 3 are not adjacent. Any plan that takes the 6 at index 2 blocks both of them and can never reach 40.
EXAMPLE 4
Input: nums = [4]
Output: 4
A single house has no neighbours, so it is always safe to rob.
EXAMPLE 5
Input: nums = [0, 0, 0]
Output: 0
Empty houses are allowed. Robbing nothing and robbing everything both total zero, so the answer is zero rather than an error.
Does adjacency mean physically nearby, or next to each other in the array?
Next to each other in the array — only indices i and i+1 conflict. Houses 0 and 2 can both be robbed no matter how the street is drawn.
Are the houses in a line or a circle?
A line here, so the first and last houses are unrelated. If it were a circle they would conflict, and the standard fix is to solve the line problem twice, once dropping the first house and once dropping the last, then take the better result.
Can the amounts be zero or negative?
Zero is allowed. Negative values are not in this problem, and if an interviewer allowed them you would simply never rob a negative house — the max already handles it, since skipping is always available.
Do I need to return which houses were robbed, or just the total?
Just the total. If the actual set were required you would keep a parent pointer alongside each table entry and walk backwards from the end, which is the standard way to recover a solution from any of these tables.
What is actually being chosen

You get to pick a set of houses. The only rule is that no two picked houses may sit side by side in the array. Your score is the sum of what you picked, and you want that score as large as possible.

Two things to be clear about before starting. Adjacent means adjacent in the array, so you can absolutely rob houses 0 and 2, or 0 and 5 — the gap does not need to be small, it just needs to exist. And the houses form a line, not a loop, so the first and last houses are not neighbours. If they were, this would be a genuinely harder problem.

Why the greedy instincts fail

Most people reach first for a simple rule. Every simple rule breaks, and it is worth seeing exactly how, because it explains why we need to do real work.

"Rob every other house." On [2, 1, 1, 2] that takes indices 0 and 2 for a total of 3. But 0 and 3 are not adjacent, and they give 4.

"Always grab the biggest remaining house, then cross out its neighbours." On [2, 7, 9, 3, 1] that takes 9 first, kills 7 and 3, then takes 2 and 1 for a total of 12 — which happens to be right here. But on [8, 10, 8] it grabs the 10, crosses out both 8s, and stops at 10. Yet the two 8s sit at indices 0 and 2, which are not adjacent, so taking both gives 16.

The pattern in both failures is the same: a choice that looks best right now can lock you out of a better choice later. When that is true, no fixed rule will do, and you have to compare the futures that each choice leads to.

The state, as a story

I walk the street one house at a time. Standing at house i, I have two choices:

- Rob house i. I grab nums[i], but house i-1 is now off-limits (linked alarm), so I jump back to my best result up to house i-2.
- Skip house i. I take nothing here and carry on with my best result up to house i-1.

I keep the bigger of the two. The only thing I need to track is which house I'm standing at — one index.

State Definitionf(i) is the most money I can rob from houses 0 through i, with no two robbed houses adjacent.

f(i) = max( nums[i] + f(i-2), f(i-1) )

Base cases: f(0) = nums[0] — one house, take it. f(1) = max(nums[0], nums[1]) — two houses side by side, so keep the richer.

Why "f(i-2)" and not "f(i-2) with house i-2 definitely robbed"

This point is worth a moment. When you rob house i and jump back to f(i-2), you are not demanding that house i-2 was robbed. You are only saying house i-1 was not. f(i-2) is the best plan over all houses up to i-2. None of those plans can touch house i-1, because i-1 is not in that range at all. So robbing i on top of f(i-2) is always legal. This is why one index is enough for the state: the "is my neighbour free?" question is answered by where the range ends, not by remembering what we picked.

Start with plain recursion

The plain recursion writes itself. It is exponential for the usual reason: two branches per call, n deep, but only n truly different questions inside.

python
def rob(nums):
    def f(i):
        if i < 0: return 0
        if i == 0: return nums[0]
        return max(nums[i] + f(i - 2), f(i - 1))
    return f(len(nums) - 1)
Memoisation: the notebook (top-down)

The trouble is repetition. f(4) calls f(3) and f(2). But f(3) also calls f(2). So f(2) is computed from scratch more than once, and its answer never changes. There are only n different questions in here — f(0) through f(n-1) — yet the recursion asks them again and again. So keep a notebook. Before you compute f(i), check the notebook. If it is there, return it. If not, compute it once and write it down. That one change drops the work to O(n):

python
def rob(nums):
    memo = {}
    def f(i):
        if i < 0: return 0
        if i == 0: return nums[0]
        if i in memo: return memo[i]
        memo[i] = max(nums[i] + f(i - 2), f(i - 1))
        return memo[i]
    return f(len(nums) - 1)
Filling a table (bottom-up)

Now remove the recursion and build the answers in an array instead. Keep the same meaning: let dp[i] hold exactly what f(i) returned — the most money you can rob from houses 0 through i. Then read the recurrence as an assignment. The recurrence said

f(i) = max( nums[i] + f(i-2), f(i-1) )

so, swapping each function call for an array read, the very same line becomes

dp[i] = max( nums[i] + dp[i-2], dp[i-1] )

It is the identical formula. We just store each result in a slot instead of returning it.

Which order do we fill the slots? dp[i] needs dp[i-1] and dp[i-2], both to its left. So if we fill left to right, both are ready by the time we reach i. No recursion, and no call stack.

That leaves the first two slots, which the formula cannot produce — it would reach back to indices -1 and -2, which do not exist. So we fill them by hand first:

- dp[0] is the most money from house 0 by itself. There is only one house and no neighbour to clash with, so rob it: dp[0] = nums[0].
- dp[1] is the most money from houses 0 and 1. They sit side by side, so you may keep only one — take the richer of them: dp[1] = max(nums[0], nums[1]).

With those two seeded, the loop starts at i = 2 and applies the formula the rest of the way:

python
def rob(nums):
    n = len(nums)
    if n == 1: return nums[0]
    dp = [0] * n
    dp[0] = nums[0]
    dp[1] = max(nums[0], nums[1])
    for i in range(2, n):
        dp[i] = max(nums[i] + dp[i - 2], dp[i - 1])
    return dp[n - 1]
Shrinking the memory

Last, look at what one step of that loop actually reads: dp[i-1] and dp[i-2], never anything older. So the full array is wasted space — two variables hold everything the loop will ever look at. Keep two_back and one_back, slide them forward each step, and the memory drops to O(1):

python
def rob(nums):
    two_back, one_back = 0, 0
    for money in nums:
        current = max(money + two_back, one_back)
        two_back, one_back = one_back, current
    return one_back

Starting both variables at 0 lets the loop handle the first two houses without special cases. On the first pass, max(nums[0] + 0, 0) is just nums[0], which is right. On the second, max(nums[1] + 0, nums[0]) is the richer of the two, also right.

Worked Example:nums = [2, 7, 9, 3, 1]
- House 0 (2): rob it (2 + 0) versus skip (0). Best so far: 2.
- House 1 (7): rob it (7 + 0 = 7) versus skip and keep 2. Best so far: 7.
- House 2 (9): rob it (9 + 2 = 11 — the 2 comes from two houses back, which is legal) versus skip and keep 7. Best so far: 11.
- House 3 (3): rob it (3 + 7 = 10) versus skip and keep 11. Best so far: 11. Here the table correctly refuses a house that looks profitable.
- House 4 (1): rob it (1 + 11 = 12) versus skip and keep 11. Best so far: 12.

Answer 12, from houses 0, 2 and 4. Try the same walk on [5, 20, 6, 20, 5]: the running best yields 5, 20, 20, 40, 40, so the answer is 40. It correctly takes both 20s at indices 1 and 3, which are not adjacent to each other.

The reflex to build

The idea that travels is not "houses". It is this: when a rule links neighbouring choices, decide at each position between taking it (and jumping back past the forbidden neighbour) and leaving it (and keeping the previous best). Whenever a problem says "you cannot pick two things next to each other", or "you must wait k turns after choosing", reach for this shape. The gap in the index is just how far the rule reaches.

Two natural twists test the same idea. If the street were a circle, the first and last houses would clash. The fix is to run this same routine twice — once without the last house, once without the first — and take the better result. If the houses formed a tree instead of a line, the same take-or-skip choice applies at each node; "skip past the neighbour" just becomes "skip past the children". Same idea, different shape of neighbourhood.

Exponential Try Every Subset
O(N) Time · O(N) Space Memoisation
O(N) Time · O(N) Space Tabulation
O(N) Time · O(1) Space Two Variables