Problem
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. All houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, adjacent houses have a security system connected, and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given an integer array nums
representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.
Examples
Example 1:
Input: nums = [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.
Example 2:
Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3). Total amount you can rob = 1 + 3 = 4.
Example 3:
Input: nums = [1,2,3]
Output: 3
This is an extension of House Robber
Similar Problem
House Robber House Robber 3 - Houses in Binary Tree House Robber 4
Solution
Method 1 - Naive
Since the houses are arranged in a circle, the first and last houses are adjacent, so they cannot both be robbed on the same night. This requires us to divide the problem into two subproblems:
- Rob houses from index
0
ton-2
(excluding the last house). - Rob houses from index
1
ton-1
(excluding the first house).
The result will be the maximum of these two subproblems.
Naive Recursive Approach
To solve it naively and recursively:
- Define a helper function to determine the maximum sum that can be robbed from a linear segment of houses.
- Recursively decide whether to rob the current house or skip it.
- Compute the above two cases (excluding the last house and excluding the first house) and return the maximum.
Code
Java
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
return Math.max(robHelper(nums, 0, nums.length - 2), robHelper(nums, 1, nums.length - 1));
}
private int robHelper(int[] nums, int start, int end) {
if (start > end) return 0;
// Option 1: Rob current house
int rob = nums[start] + robHelper(nums, start + 2, end);
// Option 2: Skip current house
int skip = robHelper(nums, start + 1, end);
return Math.max(rob, skip);
}
}
Python
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
def rob_helper(nums: List[int], start: int, end: int) -> int:
if start > end:
return 0
# Rob current house or skip it
rob = nums[start] + rob_helper(nums, start + 2, end)
skip = rob_helper(nums, start + 1, end)
return max(rob, skip)
# Max between robbing from 0 to n-2 or 1 to n-1
return max(rob_helper(nums, 0, len(nums) - 2), rob_helper(nums, 1, len(nums) - 1))
Complexity
- ⏰ Time complexity:
O(2^n)
. Divide into two subproblems (rob excluding the last house and rob excluding the first house), solved recursively. So, due to branching out, TC isO(2^n)
. - 🧺 Space complexity:
O(n)
, due to the call stack.
Method 2 - Top Down DP with memoization
Memoization can optimise the naive recursion by avoiding repeated calculations for the same subproblems. Here’s how we’ll modify our approach:
- Use a helper function with start and end indices for a linear segment of the houses.
- Use a memoization table (array or dictionary) to store results of previously computed subproblems. Specifically, we memoise the result for each starting index to avoid redundant calculations.
- Compute the maximum result for the two cases (excluding the last house and excluding the first house).
- Return the maximum of the two results.
Code
Java
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
return Math.max(robHelper(nums, 0, nums.length - 2, new HashMap<>()),
robHelper(nums, 1, nums.length - 1, new HashMap<>()));
}
private int robHelper(int[] nums, int start, int end, HashMap<Integer, Integer> memo) {
if (start > end) return 0;
if (memo.containsKey(start)) return memo.get(start);
// Option 1: Rob current house
int rob = nums[start] + robHelper(nums, start + 2, end, memo);
// Option 2: Skip current house
int skip = robHelper(nums, start + 1, end, memo);
int ans = Math.max(rob, skip);
memo.put(start, ans);
return ans;
}
}
Python
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
def rob_helper(nums: List[int], start: int, end: int, memo: dict[int, int]) -> int:
if start > end:
return 0
if start in memo:
return memo[start]
# Rob current house or skip it
rob = nums[start] + rob_helper(nums, start + 2, end, memo)
skip = rob_helper(nums, start + 1, end, memo)
memo[start] = max(rob, skip)
return memo[start]
# Max between robbing from 0 to n-2 or 1 to n-1
return max(rob_helper(nums, 0, len(nums) - 2, {}), rob_helper(nums, 1, len(nums) - 1, {}))
Complexity
- ⏰ Time complexity:
O(n)
. Each house (or subproblem) is computed only once, so the time complexity is O(n), wheren
is the number of houses.- Looking up and updating the memo table takes constant time for each house.
- 🧺 Space complexity:
O(n)
for using memoisation table and recursion call stack in worst case.
Method 3 - Bottom up DP
The bottom-up approach eliminates recursion by iteratively solving the problem while keeping track of previous results. For the House Robber II problem, we’ll calculate the maximum amounts using two linear DP arrays—one excluding the last house and one excluding the first house.
- Key Idea: Dynamically compute results for each house by maintaining the maximum money that can be robbed up to that house.
Steps For Bottom-Up Approach
If there is only one house, return its value (
nums[0]
).Use the helper function twice for two subproblems:
- One for robbing houses
[0, n-2]
(excluding the last house). - The other for robbing houses
[1, n-1]
(excluding the first house).
- One for robbing houses
In the helper function (for linear robbery problem):
- Use two variables (
prev1
andprev2
) to store the maximum amount robbed up to the last two houses. - For each house, decide whether to rob it or skip it.
- Use two variables (
Return the maximum of the two computed results.
Code
Java
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
return Math.max(robHelper(nums, 0, nums.length - 2), robHelper(nums, 1, nums.length - 1));
}
private int robHelper(int[] nums, int start, int end) {
int prev1 = 0, prev2 = 0;
for (int i = start; i <= end; i++) {
int curr = Math.max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
}
Python
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
def rob_helper(nums: List[int], start: int, end: int) -> int:
prev1, prev2 = 0, 0
for i in range(start, end + 1):
curr = max(prev1, prev2 + nums[i])
prev2 = prev1
prev1 = curr
return prev1
# Max between robbing from 0 to n-2 or 1 to n-1
return max(rob_helper(nums, 0, len(nums) - 2), rob_helper(nums, 1, len(nums) - 1))
Complexity
- ⏰ Time complexity:
O(n)
. Each house is visited once in both subproblems (excluding first or last house). - 🧺 Space complexity:
O(1)
. Only two variables are used to store intermediate results.