The Quest Begins (The "Why")
I still remember the first time I saw a DP problem in an interview. The interviewer slid a whiteboard marker toward me and said, “Given an array of house values, return the maximum amount of money you can rob without hitting two adjacent houses.” My brain instantly went into loop‑over‑loop mode: try every subset, check adjacency, keep the best. I coded a brute‑force recursion, watched the exponential tree explode, and felt like Neo dodging bullets in slow motion—except the bullets were test cases timing out.
That moment taught me a hard lesson: knowing how to memoize isn’t enough; you need to grasp why the subproblem structure collapses into a simple recurrence. Once you see the pattern, the solution feels less like magic and more like a obvious next step.
The Revelation (The Insight)
The House Robber problem is a perfect illustration of DP’s core idea: optimal substructure + overlapping subproblems.
Consider the i‑th house. When we reach it, there are only two mutually exclusive possibilities for an optimal solution up to i:
- We rob house i. Then we cannot rob house i‑1, so the best we can do is the value of house i plus the best outcome for houses up to i‑2.
- We skip house i. Then the best outcome is simply whatever we achieved up to house i‑1.
Let rob(i) be the max money we can collect from houses [0…i]. The recurrence is:
rob(i) = max( value[i] + rob(i-2), rob(i-1) )
Base cases:
-
rob(-1) = 0(no houses) -
rob(0) = value[0](only the first house)
Now notice that to compute rob(i) we only ever need the two previous results (rob(i-1) and rob(i-2)). We don’t need an entire table—just two rolling variables. That’s the state‑compression trick that turns O(n) space into O(1) while keeping O(n) time.
Why does this work? Because the decision at each house only depends on the immediate past; older houses are already summarized in those two variables. No information is lost, and we never recompute the same subproblem twice.
Wielding the Power (Code & Examples)
The Struggle (Naïve Recursion)
def rob_naive(nums, i):
if i < 0:
return 0
if i == 0:
return nums[0]
# try robbing i or skipping i
return max(nums[i] + rob_naive(nums, i-2), rob_naive(nums, i-1))
This is clean, but each call spawns two more, leading to O(2ⁿ) time—utterly unusable for n > 30.
The Victory (DP with State Compression)
def rob(nums):
"""
Returns the maximum amount of money that can be robbed
without robbing two adjacent houses.
"""
prev_two, prev_one = 0, 0 # rob(i-2), rob(i-1)
for money in nums:
# If we rob this house, we add to prev_two;
# if we skip, we keep prev_one.
current = max(money + prev_two, prev_one)
# shift the window forward
prev_two, prev_one = prev_one, current
return prev_one
Why it feels like a win:
-
O(n) time: one pass through
nums. - O(1) space: only two integers survive across iterations.
-
No hidden traps: we correctly handle empty list (
prev_onestays 0) and single‑element list.
Common Pitfalls (The Traps)
| Trap | What happens | Fix |
|---|---|---|
Using an array dp[i] and forgetting to initialize dp[-1]
|
IndexError or wrong base case | Seed dp[0] = nums[0], dp[1] = max(nums[0], nums[1]) or just use two vars. |
Updating variables in the wrong order (prev_one = current; prev_two = prev_one) |
You lose the true i-2 value |
Update simultaneously: prev_two, prev_one = prev_one, current. |
| Assuming you must rob the first house | Misses solutions that skip the first house | The recurrence already covers both rob/skip cases; no extra condition needed. |
A Second Interview Twist: House Robber II
Now the houses form a circle—the first and last houses are adjacent. The trick? Break the circle into two linear subproblems:
- Rob houses
[0 … n‑2](exclude last). - Rob houses
[1 … n‑1](exclude first).
Answer is the max of the two. Re‑use the same rob helper:
def rob_circle(nums):
if len(nums) == 1:
return nums[0]
return max(rob(nums[:-1]), rob(nums[1:]))
Still O(n) time, O(1) extra space—just two linear scans.
Why This New Power Matters
Mastering this pattern does more than let you ace a LeetCode medium. It trains you to spot the “two‑state” shape in countless problems:
- Delete and Earn → transform frequencies into a House Robber‑like DP.
- Maximum Sum of Non‑Adjacent Elements → identical recurrence.
- Stock trading with cooldown → three states, same rolling‑window idea.
When you internalize the why—that each step only needs a bounded summary of the past—you stop memorizing templates and start designing them on the fly. That’s the real superhero upgrade: turning a daunting exponential search into a calm, linear walk through the data.
Your Turn
Grab a piece of paper (or your favorite IDE) and try this:
Given an array of integers, find the maximum sum of a subsequence where no two elements are adjacent.
(Hint: it’s exactly House Robber.)
Implement it, test it on edge cases ([], [5], [1,2,3,1]), then attempt the circular variant.
When you see the solution snap into place with just two variables, you’ll feel that same rush Neo felt when he finally saw the Matrix. Happy coding, and may your DP always be O(n)! 🚀
Top comments (0)