DSA Guide
Dynamic Programming

House Robber

Pick non-adjacent numbers for the largest total, and meet the take-it-or-skip-it decision that defines dynamic programming

MediumDynamic Programming· take or skip
ArrayDynamic Programming
Problem #198

Problem Statement

You are given an array where nums[i] is the amount of money in house i. You cannot take from two adjacent houses. Return the maximum you can collect.

Input:  nums = [1, 2, 3, 1]
Output: 4
Why:    take house 0 (1) and house 2 (3)

Input:  nums = [2, 7, 9, 3, 1]
Output: 12
Why:    take 2 + 9 + 1, not 7 + 3

Intuition

The tempting rule is "always take the biggest available house". Watch it fail:

Why greedy is wrong here

nums = [2, 7, 9, 3, 1]
        0  1  2  3  4

Greedy: take 9 (the largest), which blocks 7 and 3.
        Then take 2 and 1  →  total 12.
Best:   2 + 9 + 1                →  12.   Greedy is right, by luck.

Now the same rule on a different array:

nums = [6, 10, 6, 1, 1]
        0   1  2  3  4

Greedy: take 10 (the largest), which blocks BOTH sixes.
        Only index 3 or 4 is left  →  total 11.
Best:   6 + 6 + 1  (indices 0, 2, 4)  →  13.

Taking the single biggest number cost more than it gained, because it blocked two others that were worth more together.

Local best is not global best, so greedy does not apply.

Since you cannot decide greedily, you must consider both options — and that is the shape of dynamic programming.

The key insight

Standing at house i, there are exactly two choices:

  • Take it — collect nums[i], plus the best possible from houses 0 … i-2 (skipping the neighbour).
  • Skip it — collect the best possible from houses 0 … i-1.

The answer at i is the larger of the two:

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

That single line is the entire algorithm. Everything else is bookkeeping.

Why this is DP and not just recursion

Write that formula as plain recursion and best(3) gets computed by best(5), and again by best(4), and so on — the same subproblems recomputed exponentially many times.

Both DP conditions hold:

ConditionHere
Optimal substructure — big answers built from small onesbest(i) is built from best(i-1) and best(i-2)
Overlapping subproblems — small ones repeatbest(i-2) is needed by both best(i) and best(i-1)

So compute each best(i) once, in order, and reuse it.

The real-world version

You are choosing which shifts to work. Two shifts back to back are not allowed. Standing at Friday, you ask: is it better to work Friday plus my best through Wednesday, or to skip Friday and keep my best through Thursday?

You do not need to remember which specific days you picked — only the two running totals.

Approach

Notice only two previous answers matter

best(i) depends on best(i-1) and best(i-2). Nothing older is ever consulted.

So the whole table collapses into two variables.

Track prev and prev2

prev is the best through the previous house; prev2 is the best through the one before that. Both start at 0, which correctly represents "no houses yet".

At each house, take the better of the two options

current = max(num + prev2, prev).

Shift the window forward

prev2 = prev, then prev = current. Order matters — overwriting prev first would lose the value prev2 needs.

Watch it run

nums = [2, 7, 9, 3, 1]
0
2
i
1
7
2
9
3
3
4
1
prev20prev0take2skip0best2
House 0. Take = 2 + 0 = 2. Skip = 0. Taking wins, so best is 2.
1 / 5
Two variables replace the whole table, because nothing older than two steps is ever needed.

Solution

def rob(nums: list[int]) -> int:
    """Maximum sum of non-adjacent values, in O(1) space."""
    prev2 = 0  # best through house i-2
    prev = 0   # best through house i-1

    for num in nums:
        # Take this house (skipping the neighbour) or skip it.
        current = max(num + prev2, prev)

        # Shift the window. prev2 must be updated BEFORE prev is overwritten.
        prev2 = prev
        prev = current

    return prev


def rob_table(nums: list[int]) -> int:
    """The same recurrence with an explicit table — clearer, uses O(n) space."""
    if not nums:
        return 0

    n = len(nums)
    best = [0] * (n + 1)
    best[1] = nums[0]

    for i in range(2, n + 1):
        # best[i] means "best using the first i houses".
        best[i] = max(nums[i - 1] + best[i - 2], best[i - 1])

    return best[n]

The update order is a real bug source

prev = current      ← WRONG if done first
prev2 = prev        ← now prev2 gets the NEW value, not the old one

Both variables end up holding the same thing, and the adjacency rule silently stops being enforced. The result is too large, with no error.

Update prev2 first, or use simultaneous assignment: prev2, prev = prev, current.

Complexity Analysis

Time Complexity

O(n)

Space Complexity

O(1)

  • Time — one pass, constant work per house.
  • Space — two variables, regardless of input size. The table version is O(n) and is worth writing first if the recurrence is not yet obvious; collapsing to two variables is a mechanical last step.

The four stages, on this problem

StageCostWhat it looks like
Naive recursionO(2ⁿ)rob(i) = max(nums[i] + rob(i-2), rob(i-1))
MemoizedO(n) time, O(n) spaceSame, plus a cache
TabulatedO(n) time, O(n) spaceThe loop with a best array
Space-optimisedO(n) time, O(1) spaceTwo variables

Every stage is correct. Stop wherever the constraints allow.

Edge Cases

On this page