You are given an integer array nums where nums[i] represents the amount of money the ith house has. The houses are arranged in a straight line, i.e. the ith house is the neighbor of the (i-1)th and (i+1)th house.
You are planning to rob money from the houses, but you cannot rob two adjacent houses because the security system will automatically alert the police if two adjacent houses were both broken into.
Return the maximum amount of money you can rob without alerting the police.
Example 1:
Input: nums = [1,1,3,3]
Output: 4Explanation: nums[0] + nums[2] = 1 + 3 = 4.
Example 2:
Input: nums = [2,9,8,3,6]
Output: 16Explanation: nums[0] + nums[2] + nums[4] = 2 + 8 + 6 = 16.
Constraints:
1 <= nums.length <= 1000 <= nums[i] <= 100
You should aim for a solution as good or better than O(n) time and O(n) space, where n is the number of houses.
Can you think of this problem in terms of recursion? Consider drawing a decision tree where, at each step, we can choose to rob the house or skip it. If we rob the current house, we cannot rob the next or the previous house. Can you derive a recurrence relation to solve the problem?
We can recursively start from the first house and branch paths accordingly. If we rob the current house, we skip the next house; otherwise, we move to the next house. The recurrence relation can be expressed as max(nums[i] + dfs(i + 2), dfs(i + 1)), where i is the current house and dfs is the recursive function. Can you determine the base condition to stop the recursion?
The base condition would be to return 0 when i goes out of bounds. This recursion can leads to O(2^n) time solution. Can you think of a better way? Maybe you should try to avoid recalculating the result for a recursive call.
We can use Memoization to avoid recalculating the result multiple times for a recursive call. By storing the result of each recursive call in a hash map or an array using i as the parameter, we can immediately return the stored result if the recursion is called with the same i value again. Further optimization can be achieved using advanced techniques like Bottom-Up dynamic programming.
Before attempting this problem, you should be comfortable with:
At every house, you have two choices:
The goal is to choose the option that gives the maximum total money.
Recursion tries both choices at each index and returns the best result.
0.i:i + 1nums[i] + solve(i + 2)i goes out of bounds, return 0.The recursive solution recomputes the same subproblems many times.
To optimize this, we store the result for each index once it’s computed.
At every house i, you still have two choices:
i + 1nums[i] and go to i + 2Using memoization, each index is solved only once.
memo[i] stores the maximum money from house i.dfs(i):i is out of bounds, return 0.memo[i] is already computed, return it.skip = dfs(i + 1)rob = nums[i] + dfs(i + 2)max(skip, rob) in memo[i].0.Instead of deciding recursively, we build the answer step by step.
For each house i, the maximum money we can have depends on:
i - 1i + best up to i - 2We choose the better of the two at every step.
0dp[i] = max money up to house i.dp[0] = nums[0]dp[1] = max(nums[0], nums[1])i from 2 to n - 1:dp[i] = max(dp[i - 1], nums[i] + dp[i - 2])dp[n - 1].class Solution:
def rob(self, nums: List[int]) -> int:
if not nums:
return 0
if len(nums) == 1:
return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i - 1], nums[i] + dp[i - 2])
return dp[-1]We don’t actually need a full DP array.
At any house, we only care about:
So instead of storing everything, we just keep two variables and update them as we move forward.
For each house:
rob1 → best up to house i - 2rob2 → best up to house i - 1newRob = max(rob2, rob1 + currentHouseValue)rob1 = rob2rob2 = newRobrob2 is the answer.In the bottom-up DP approach, dp[1] should be initialized as max(nums[0], nums[1]), not just nums[1]. This represents the maximum money obtainable from the first two houses. If you set dp[1] = nums[1], you ignore the possibility that the first house might have more money, leading to suboptimal results.
The recurrence dp[i] = max(dp[i-1], nums[i] + dp[i-2]) represents choosing between skipping house i (keeping the best from i-1) or robbing house i (adding its value to the best from i-2). A common mistake is writing dp[i] = max(dp[i-1] + nums[i], dp[i-2]), which incorrectly adds the current house value when skipping it. Remember: robbing requires jumping over the previous house, not adding to it.
When the input array is empty, return 0. When it has only one element, return that element. The bottom-up approach with a DP array requires at least two elements to initialize dp[0] and dp[1]. Failing to handle these edge cases causes index-out-of-bounds errors or incorrect results. The space-optimized solution with two variables naturally handles the single-element case but still needs an empty array check.