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 circle, i.e. the first house and the last house are neighbors.
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 = [3,4,3]
Output: 4Explanation: You cannot rob nums[0] + nums[2] = 6 because nums[0] and nums[2] are adjacent houses. The maximum you can rob is nums[1] = 4.
Example 2:
Input: nums = [2,9,8,3,6]
Output: 15Explanation: You cannot rob nums[0] + nums[2] + nums[4] = 16 because nums[0] and nums[4] are adjacent houses. The maximum you can rob is nums[1] + nums[4] = 15.
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.
First, consider solving the problem to get the maximum money after robbing without the condition that 'the first and last houses are adjacent'. Can you express this using a recurrence relation? Perhaps you could draw a decision tree, as at each step, you can either rob the current house and skip the next one or skip the current house and move to the next.
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. The base condition for this recursion would be to return 0 when i goes out of bounds. This solution results in O(2^n) time complexity because, at each recursive step, we branch into two paths. Can you think of a way to avoid recalculating the result for the same recursive call multiple times?
We can use memoization to store the result of a recursive function in a hash map or an array and immediately return this value when the function is called again with the same parameter values. How would you implement this? How would you solve the problem if the first and last houses are adjacent to each other? Perhaps you should consider skipping any one house between the two.
We can create two arrays from the given array. The first will include houses from the first house to the second-to-last house, and the second will include houses from the second house to the last house. We can run the recursive function on both arrays independently and return the maximum result between the two. Advanced techniques such as bottom-up dynamic programming can further optimize the solution.
Before attempting this problem, you should be comfortable with:
This is House Robber II, where houses are in a circle.
So the first and last house cannot both be robbed.
To handle the circular constraint, we split the problem into two linear cases:
0 to n-2 (exclude last house)1 to n-1 (exclude first house)The recursive function explores:
A flag is used to ensure that if the first house is robbed, the last house is not allowed.
Finally, we take the maximum result from the two cases.
0 (first house considered)1 (first house skipped)class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
def dfs(i, flag):
if i >= len(nums) or (flag and i == len(nums) - 1):
return 0
return max(dfs(i + 1, flag),
nums[i] + dfs(i + 2, flag or i == 0))
return max(dfs(0, True), dfs(1, False))This is House Robber II (circular houses) with Top-Down DP.
Because houses form a circle, the first and last houses cannot both be robbed.
We handle this by tracking a flag that tells us whether the first house was robbed.
At each house, we have two choices:
Memoization is used so each state (index, flag) is solved only once.
memo[index][flag]:index -> current houseflag -> whether the first house was robbed0 (first house included)1 (first house excluded)class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
memo = [[-1] * 2 for _ in range(len(nums))]
def dfs(i, flag):
if i >= len(nums) or (flag and i == len(nums) - 1):
return 0
if memo[i][flag] != -1:
return memo[i][flag]
memo[i][flag] = max(dfs(i + 1, flag),
nums[i] + dfs(i + 2, flag or (i == 0)))
return memo[i][flag]
return max(dfs(0, True), dfs(1, False))This is House Robber II (circular houses) solved using Bottom-Up Dynamic Programming.
Because houses are in a circle, you cannot rob both the first and last house.
So we split the problem into two linear cases:
Each case becomes the normal House Robber I problem.
nums[1:]nums[:-1]dp[i] = maximum money up to house idp[i] = max(dp[i-1], nums[i] + dp[i-2])class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
return max(self.helper(nums[1:]),
self.helper(nums[:-1]))
def helper(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]This is House Robber II, where houses are arranged in a circle.
Because of the circular setup, you cannot rob both the first and last house.
To handle this, split the problem into two linear subproblems:
Each subproblem becomes the classic House Robber I, which can be solved using two variables instead of a full DP array.
nums[1:] (skip first)nums[:-1] (skip last)rob1 -> best up to house i-2rob2 -> best up to house i-1newRob = max(rob1 + current_house, rob2)
rob1 = rob2
rob2 = newRobWhen there is only one house, the circular constraint does not apply since there is no adjacency conflict. If you split the problem into nums[1:] and nums[:-1] without handling this case, both subarrays become empty when n=1, and the function returns 0 instead of nums[0]. Always check if the array has only one element and return its value directly.
The key insight is that the first and last houses cannot both be robbed. A common mistake is to add complex flag logic throughout the recursion when a simpler approach exists: run the standard House Robber algorithm twice, once excluding the first house and once excluding the last house. Taking the maximum of these two results correctly handles the circular dependency without overcomplicating the code.
When splitting the array into nums[1:] (exclude first) and nums[:-1] (exclude last), be careful with language-specific slicing syntax. In some languages, you need to manually copy subarrays or adjust loop bounds. An off-by-one error here can lead to including both the first and last houses in one subproblem, or excluding more houses than intended, producing incorrect results.