Lesson 48 — DP II: take it or skip it

~20 minutes · Day 62 · 1-D DP · Python

Tonight you meet House Robber: every house has money, but robbing two adjacent houses triggers the alarm. Interviewers like this because the local choice sounds simple, yet one greedy move can ruin the total. The pattern is to make the choice at each house while carrying the best totals from smaller prefixes.

The invariant

At each house choose: rob it and add dp[i-2], or skip it and keep dp[i-1] — dp[i] = max(dp[i-1], nums[i] + dp[i-2]).

That one sentence is the whole lesson. Dynamic programming is useful when smaller overlapping subproblems can be solved once and reused (Wikipedia: dynamic programming). Here the repeated question is: “what is the best total up to this house?”

The four DP questions

House Robber DP table transition A DP table shows dp of i equals the max of skipping from dp i minus one or taking nums i plus dp i minus two. Example nums = [2, 7, 9, 3, 1] dp table being filled i-2 i-1 i i+1 i+2 dp=2 dp=7 dp=? later later take: nums[i] + dp[i-2] = 9 + 2 = 11 skip: dp[i-1] = 7 transition: dp[i] = max(skip, take)

House Robber: the slow way first

The brute force recursion is O(2n): at each house, branch into two futures. That is easy to reason about, but it keeps asking the same suffix questions again and again.

def best_from(i):
    if i >= len(nums): return 0
    take = nums[i] + best_from(i + 2)   # rob this house
    skip = best_from(i + 1)             # leave this house
    return max(take, skip)

The better version keeps only the two earlier answers the transition needs. Python's max gives the larger of the skip and take totals.

House Robber: the DP way

def rob(nums):
    prev2 = 0                       # best up to i-2
    prev1 = 0                       # best up to i-1
    for money in nums:
        take = money + prev2        # rob current house
        skip = prev1                # skip current house
        cur = max(skip, take)       # dp[i]
        prev2, prev1 = prev1, cur   # slide the window
    return prev1

The trust point is the update order. prev2 and prev1 must still mean “two houses back” and “one house back” while you compute cur. Only after that do they slide forward.

Transfer: same choice, new costume

A row of donation boxes has amounts written on them. You may take money from any boxes, but taking neighboring boxes is not allowed. Return the maximum amount.

Before opening the reveal: what should dp[i] mean, and what two choices should the transition compare?

Your prediction first — then reveal.

It is the same pattern. Let dp[i] mean the best total from boxes 0..i. For each box, compare skipping it (dp[i-1]) with taking it (boxes[i] + dp[i-2]). The story changed; the invariant did not.

What the trade buys you

ApproachTimeSpace
Brute force (branch take or skip)O(2n)O(n)
DP (reuse the last two answers)O(n)O(1)

The DP version spends no extra table because this transition only needs the previous two states. If writing the full dp array helps you see it first, do that; then compress it to two variables.

Practice — answer before you scroll past

1. What does dp[i] mean in House Robber?

2. At house i, which choices are compared?

3. Spot the bug in this version:

prev2 = 0
prev1 = 0
for money in nums:
    cur = max(prev1, money + prev2)
    prev2 = cur
    prev1 = cur
return prev1

4. Which prompt should make you think take-or-skip DP?

Your move (tonight)

Solve House Robber on LeetCode, logging it in your Notion tracker with the cycle — predict → attempt → compare → encode:

  1. Predict: say the state, transition, base case, and answer location out loud.
  2. Attempt: write the two-variable DP from memory, then submit.
  3. Compare: check whether your update order matches the solution.
  4. Encode: write the invariant from this page in your own words.
One primary source

Watch NeetCode — House Robber after your attempt, as the compare step. Listen for how he names the same take-or-skip recurrence.