Repository navigation
132 — House Robber II
LeetCode 213 · Medium. All houses are arranged in a circle — the first house and the last house are adjacent neighbors. Given an integer array nums representing the amount of money at each house, return the maximum amount of money you can rob tonight without robbing two adjacent houses (including the first-and-last wraparound pair).
The only new wrinkle over House Robber is that the street bends into a loop, so house 0 and the last house are now neighbors too. That one extra adjacency ruins the clean left-to-right sweep — until you notice that any valid robbery plan either leaves house 0 alone, or leaves the last house alone (it can never rob both, since they're adjacent). So instead of solving one circular problem, solve two ordinary linear House Robber problems — "rob houses 0 through n-2" and "rob houses 1 through n-1" — and take whichever gives more. Every legitimate circular plan is captured by one of those two linear slices, and neither slice can accidentally rob both wraparound neighbors.
"Houses arranged in a circle" on top of the familiar House Robber phrasing is the cue: whenever a linear no-two-adjacent DP gets a wraparound constraint tacked on, the fix is almost always "run the linear version twice, once excluding each end, and take the max" rather than inventing a new circular recurrence from scratch.
Enumerate every subset of houses as a bitmask, keep only the subsets where no two adjacent bits are set and the first and last bits aren't both set (the circular adjacency), and take the max sum among valid subsets.
func robCircularBruteForce(_ nums: [Int]) -> Int {
let n = nums.count
guard n > 0 else { return 0 }
guard n > 1 else { return nums[0] }
var best = 0
for mask in 0..<(1 << n) {
var valid = true
var sum = 0
for i in 0..<n where valid {
guard mask & (1 << i) != 0 else { continue }
sum += nums[i]
let next = (i + 1) % n // wraps last house back to house 0
if mask & (1 << next) != 0 {
valid = false // two adjacent houses robbed (possibly wrapping around)
}
}
if valid {
best = max(best, sum)
}
}
return best
}
// smoke test
print(robCircularBruteForce([2, 3, 2])) // 3
print(robCircularBruteForce([1, 2, 3, 1])) // 4
print(robCircularBruteForce([1, 2, 3])) // 3Big-O: O(2^n · n) time — 2^n subsets, each requiring an O(n) scan to check adjacency (including the circular wrap) and sum the take. O(1) extra space beyond the loop variables.
Run the ordinary linear House Robber rolling recurrence twice — once over houses 0...n-2 (excluding the last house) and once over houses 1...n-1 (excluding the first house) — and return the larger result.
func rob(_ nums: [Int]) -> Int {
var prev2 = 0
var prev1 = 0
for num in nums {
let current = max(prev1, prev2 + num)
prev2 = prev1
prev1 = current
}
return prev1
}
func robCircular(_ nums: [Int]) -> Int {
let n = nums.count
guard n > 0 else { return 0 }
guard n > 1 else { return nums[0] }
let excludeLast = Array(nums[0..<(n - 1)])
let excludeFirst = Array(nums[1..<n])
return max(rob(excludeLast), rob(excludeFirst))
}
// smoke test — same cases as the brute force
print(robCircular([2, 3, 2])) // 3
print(robCircular([1, 2, 3, 1])) // 4
print(robCircular([1, 2, 3])) // 3Big-O: O(n) time — two linear passes, each O(n), over slices that together cover O(n) total elements. O(n) space for the two array slices (or O(1) if the linear pass is written to take start/end indices instead of copying).
The brute force treats the circular constraint literally, checking every one of the 2^n possible subsets against both the linear and the wraparound adjacency rule — correct, but paying an exponential price to rediscover something structural. The optimal version exploits a simple case split: since house 0 and the last house can never both appear in a valid answer, the circular problem is just two ordinary linear House Robber problems glued together by an outer max. Each of those linear problems is solved by the same O(n) rolling recurrence as plain House Robber — no new algorithm is needed, just running the old one twice on two overlapping-but-different slices and keeping the better result.
-
1D Dynamic Programming — the inner
robhelper is the exact same two-state rolling recurrence from chapter 26's House Robber example, reused unchanged. -
Arrays and Strings —
excludeLast/excludeFirstare array slices, and the rolling pass over each is a plain left-to-right array traversal.
House Robber II is House Robber run twice — once pretending the last house doesn't exist, once pretending the first house doesn't exist — because a valid circular plan can never rob both wraparound neighbors at once.
- Why is it safe to solve "rob houses
0...n-2" and "rob houses1...n-1" separately and just take the max, instead of needing a single recurrence that tracks whether house 0 was robbed? - For
nums = [2, 3, 2], trace bothrob(excludeLast)androb(excludeFirst)by hand. Which slice produces the winning answer of3, and why can't robbing both house 0 and house 2 ever be valid here? - What does
robCircularreturn fornums.count == 1, and why does that case need to be handled before slicing (hint: what wouldnums[0..<(n-1)]andnums[1..<n]both look like ifn == 1)?
⬅️ Previous: House Robber · Next: Longest Palindromic Substring ➡️