The Quest Begins (The "Why")
I still remember the first time I stared at a LeetCode problem titled House Robber and felt my brain short‑circuit. The prompt was simple: Given an array of non‑negative integers representing the amount of money in each house, return the maximum amount you can rob without alerting the police (i.e., you can’t rob two adjacent houses).
My first instinct? Write a recursive function that tried every combination. I imagined myself as a bounty hunter scanning a galaxy of possibilities, each branch exploding into two more—the classic exponential blow‑up. After a few minutes of watching the call stack grow like a Death Star trench, I realized I was stuck in a loop that would never finish for anything beyond size 20.
That moment sparked a question: Is there a way to reuse work I’ve already done? The answer, as many of you know, lies in dynamic programming—but not the intimidating, textbook‑y version. I wanted to understand the why behind the recurrence, not just memorize a formula. So I embarked on a mini‑quest to uncover the pattern that turns an impossible exponential search into a clean, linear sweep.
The Revelation (The Insight)
The House Robber problem hides a beautiful property: optimal substructure. If I know the best loot I can collect up to house i‑1 and up to house i‑2, then the best loot up to house i is simply the better of:
- Skip house
i→ take the best loot up toi‑1. - Rob house
i→ add its value to the best loot up toi‑2(since I can’t touchi‑1).
That’s it. No need to consider any farther back houses because any solution that ends at i must have made a decision about i‑1 (either rob it or skip it), and the part before that decision is already optimally solved.
The overlapping subproblems appear because the same sub‑array (0 … i‑2) is needed repeatedly when we evaluate different later houses. By storing just the two previous results, we avoid recomputing them—turning the naïve recursion into an O(n) walk.
Think of it like Neo in The Matrix dodging bullets: instead of trying to predict every possible trajectory, he realizes the pattern—move left, then right, then left again—based on only the immediate previous moves. Our DP does the same: we keep only the last two “states” and let them guide the next step.
Mathematically, if dp[i] is the max money robbed from houses 0 … i:
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
Base cases:
-
dp[0] = nums[0](only one house) -
dp[1] = max(nums[0], nums[1])(pick the richer of the first two)
Notice we never need the whole array; we only ever look back two steps. That lets us collapse the DP table into two variables, giving O(1) space.
Wielding the Power (Code & Examples)
The Struggle: Exponential Recursion (what NOT to do)
function robRec(nums, i) {
if (i < 0) return 0;
// rob this house or skip it
return Math.max(
robRec(nums, i - 1), // skip i
robRec(nums, i - 2) + nums[i] // take i
);
}
function rob(nums) {
return robRec(nums, nums.length - 1);
}
Running this on an array of length 30 already feels like waiting for a podrace to finish—painfully slow.
The Victory: Linear DP with O(1) Space
function rob(nums) {
if (!nums.length) return 0;
if (nums.length === 1) return nums[0];
let prev2 = nums[0]; // dp[i-2]
let prev1 = Math.max(nums[0], nums[1]); // dp[i-1]
for (let i = 2; i < nums.length; i++) {
const curr = Math.max(prev1, prev2 + nums[i]);
prev2 = prev1; // shift window
prev1 = curr;
}
return prev1;
}
Why this works: Each iteration computes dp[i] using only the two previously stored values. The loop advances exactly like a conveyor belt—no backtracking, no repeated work.
Common traps to avoid:
-
Forgetting the base case when
nums.length === 0. Returning0is correct; otherwise you’ll try to accessnums[0]and crash. -
Using an array for
dpwhen you don’t need it. It’s tempting to allocatedp = new Array(n), but that adds O(n) space for no gain. Two variables are enough.
A Second Interview Twist: House Robber II (circular street)
Now the houses form a circle—you can’t rob the first and last house together. The trick? Run the same linear DP twice:
- Exclude the first house, solve on
nums[1 … n-1]. - Exclude the last house, solve on
nums[0 … n-2].
Take the max of the two results.
function rob(nums) {
if (nums.length === 1) return nums[0];
return Math.max(
robLinear(nums.slice(1)), // skip first
robLinear(nums.slice(0, -1)) // skip last
);
}
function robLinear(arr) {
let prev2 = 0, prev1 = 0;
for (const val of arr) {
const curr = Math.max(prev1, prev2 + val);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
Still O(n) time, O(1) space—just applied to two slices.
Why This New Power Matters
Mastering this pattern does more than let you ace a LeetCode medium. It trains you to spot optimal substructure and overlapping subproblems everywhere—whether you’re scheduling tasks, allocating resources, or even designing AI that plans moves in a game.
The House Robber DP is the gateway drug to harder variants: Delete and Earn, Maximum Sum of Non‑Adjacent Elements, Stock Buy‑Sell with Cooldown, and beyond. Once you internalize the “keep only the last two states” trick, you start seeing DP as a lightweight tool rather than a heavyweight theorem.
And the best part? The solution feels earned. You’ve turned a brute‑force nightmare into a tidy, readable loop that you can explain in a coffee break. That’s the kind of clarity that makes interviews feel less like a boss fight and more like a well‑timed combo move.
Your Turn
Grab a piece of paper (or your favorite IDE) and try solving Maximum Subarray Sum (Kadane’s algorithm) using the same mindset: identify the recurrence, keep only what you need, and iterate. Drop your solution or any questions in the comments—I’d love to see how you’ve leveled up your DP game!
Happy coding, and may the DP be with you. 🚀
Top comments (0)