A clear explanation of maximizing robbed money from circularly arranged houses using dynamic programming.
Problem Restatement
We are given an integer array nums.
Each nums[i] represents the amount of money in house i.
We want to rob houses without robbing two adjacent houses.
The difference from the first House Robber problem is that the houses are arranged in a circle. That means the first house and the last house are also adjacent.
Return the maximum amount of money we can rob without alerting the police.
The official statement says the houses are arranged in a circle, so the first house is the neighbor of the last one, and adjacent robbed houses trigger the security system. Given nums, return the maximum amount we can rob tonight without alerting the police.
Input and Output
| Item | Meaning |
|---|---|
| Input | Integer array nums |
| Output | Maximum money that can be robbed |
| Constraint | Adjacent houses cannot both be robbed |
| Circular rule | House 0 and house n - 1 are adjacent |
Example function shape:
def rob(nums: list[int]) -> int:
...Examples
Example 1:
nums = [2, 3, 2]We cannot rob both 2s because the first and last houses are adjacent.
Best choice:
rob house 1
money = 3Answer:
3Example 2:
nums = [1, 2, 3, 1]Best choice:
rob house 0 and house 2
money = 1 + 3 = 4Answer:
4Example 3:
nums = [1, 2, 3]Because the houses are circular:
house 0 is adjacent to house 2We cannot rob 1 and 3 together.
Best choice:
rob house 2
money = 3Answer:
3First Thought: Use House Robber I
For a normal line of houses, the recurrence is:
best at current house = max(skip current, rob current)If we process a linear array, we can keep two values:
| Variable | Meaning |
|---|---|
prev2 | Best result up to two houses before |
prev1 | Best result up to previous house |
For each house value money:
current = max(prev1, prev2 + money)Then shift the values forward.
This solves the linear version.
Problem With Direct Linear DP
In this problem, the first and last houses are adjacent.
So a normal linear DP may choose both:
nums = [2, 3, 2]Linear DP might consider robbing:
house 0 and house 2for total:
4But that is invalid because house 0 and house 2 are neighbors in the circle.
We need to break the circular dependency.
Key Insight
Since the first and last houses are adjacent, we cannot rob both.
So every valid answer belongs to one of these two cases:
| Case | Houses considered |
|---|---|
| Exclude first house | Rob only from index 1 to n - 1 |
| Exclude last house | Rob only from index 0 to n - 2 |
Then each case becomes the normal linear House Robber problem.
Final answer:
max(
rob_linear(nums[1:]),
rob_linear(nums[:-1]),
)We also need to handle the single-house case separately.
Algorithm
- If
len(nums) == 1, returnnums[0]. - Solve the linear robbery problem on
nums[0:n-1]. - Solve the linear robbery problem on
nums[1:n]. - Return the larger result.
Linear Helper
For a linear list of houses, use:
prev2 = 0
prev1 = 0For each amount money:
current = max(prev1, prev2 + money)Meaning:
| Choice | Value |
|---|---|
| Skip current house | prev1 |
| Rob current house | prev2 + money |
Then update:
prev2 = prev1
prev1 = currentAt the end, prev1 is the best answer.
Walkthrough
Use:
nums = [1, 2, 3, 1]Because the houses are circular, split into two cases.
Case 1: exclude the last house.
[1, 2, 3]Best linear robbery:
rob 1 and 3
total = 4Case 2: exclude the first house.
[2, 3, 1]Best linear robbery:
rob 2 and 1
total = 3Take the maximum:
max(4, 3) = 4Answer:
4Correctness
Any valid robbery plan cannot include both the first house and the last house, because those two houses are adjacent in the circle.
Therefore every valid plan must fall into at least one of these two groups:
- plans that exclude the first house
- plans that exclude the last house
The algorithm solves both groups as ordinary linear House Robber problems.
For each linear subproblem, the helper is correct because at every house there are exactly two choices:
- skip the current house, keeping the best value from the previous house
- rob the current house, adding its money to the best value from two houses before
Taking the maximum of these two choices gives the best value up to the current position.
Since the circular problem is fully covered by the two linear cases, and each linear case is solved optimally, the maximum of the two results is the optimal answer for the circular problem.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We scan two linear ranges |
| Space | O(1) | Only a few variables are used |
The implementation below avoids creating slices, so extra space stays constant.
Implementation
class Solution:
def rob(self, nums: list[int]) -> int:
n = len(nums)
if n == 1:
return nums[0]
return max(
self.rob_linear(nums, 0, n - 2),
self.rob_linear(nums, 1, n - 1),
)
def rob_linear(self, nums: list[int], start: int, end: int) -> int:
prev2 = 0
prev1 = 0
for i in range(start, end + 1):
current = max(prev1, prev2 + nums[i])
prev2 = prev1
prev1 = current
return prev1Code Explanation
Handle the single-house case first:
if n == 1:
return nums[0]This is necessary because excluding either the first or last house would leave an empty range.
Then solve two linear cases:
self.rob_linear(nums, 0, n - 2)
self.rob_linear(nums, 1, n - 1)The first excludes the last house.
The second excludes the first house.
Return the better one:
return max(...)Inside rob_linear, initialize:
prev2 = 0
prev1 = 0For each house:
current = max(prev1, prev2 + nums[i])prev1 means skipping this house.
prev2 + nums[i] means robbing this house.
Then update the rolling DP state:
prev2 = prev1
prev1 = currentFinally:
return prev1Simpler Slice Version
This version is shorter, but it creates sliced lists.
class Solution:
def rob(self, nums: list[int]) -> int:
if len(nums) == 1:
return nums[0]
return max(
self.rob_linear(nums[:-1]),
self.rob_linear(nums[1:]),
)
def rob_linear(self, houses: list[int]) -> int:
prev2 = 0
prev1 = 0
for money in houses:
current = max(prev1, prev2 + money)
prev2 = prev1
prev1 = current
return prev1The index-range version is slightly more memory-efficient.
Testing
def run_tests():
s = Solution()
assert s.rob([2, 3, 2]) == 3
assert s.rob([1, 2, 3, 1]) == 4
assert s.rob([1, 2, 3]) == 3
assert s.rob([5]) == 5
assert s.rob([1, 2]) == 2
assert s.rob([2, 7, 9, 3, 1]) == 11
print("all tests passed")
run_tests()| Test | Why |
|---|---|
[2,3,2] | First and last cannot both be robbed |
[1,2,3,1] | Standard circular example |
[1,2,3] | Best single high house due to circle |
[5] | Single-house case |
[1,2] | Two adjacent circular houses |
[2,7,9,3,1] | Larger DP case |