Problem Overview
LeetCode 213 — House Robber II — is a direct extension of the classic House Robber problem. You are given an integer array nums representing the amount of money in each house along a street. The houses are arranged in a circle, meaning the first house and the last house are neighbors. You cannot rob two adjacent houses because it will alert the police. Find the maximum amount of money you can rob tonight without alerting the police.
The circular arrangement is the only difference from House Robber I (LeetCode 198). In the linear version, the first and last houses have no special relationship. Here they are adjacent, which means robbing house 0 forbids robbing house n-1 and vice versa. This single constraint is enough to invalidate the straightforward House Robber I recurrence applied to the full array.
The problem is rated Medium on LeetCode and is a frequent interview question at top tech companies. It tests whether you can identify that the circular constraint collapses into two independent linear problems — a key pattern in circular array dynamic programming. The solution runs in O(n) time and O(1) space.
- nums[i] is the amount of money in house i; houses are arranged in a circle
- Cannot rob two adjacent houses (index difference of 1, or first and last)
- First house (index 0) and last house (index n-1) are considered adjacent
- Constraints: 1 <= nums.length <= 100; 0 <= nums[i] <= 1000
- Return the maximum amount that can be robbed without alerting police
From House Robber I to II
House Robber I (LeetCode 198) solves a linear street: dp[i] = max(dp[i-1], dp[i-2] + nums[i]). The key recurrence says: at each house, either skip it and keep the best from the previous house, or rob it and add it to the best from two houses back. Because the street is linear, there is no constraint linking the first and last house.
In House Robber II, houses form a ring. The only new constraint is that house 0 and house n-1 are adjacent — you can't rob both. Every other adjacency constraint is identical to the linear version. So the circular problem reduces to: what is the best we can do if we definitively exclude house n-1 from consideration? What if we definitively exclude house 0? The answer to House Robber II is the maximum of these two linear subproblems.
This reduction works because the optimal solution either robs house 0 or it does not. If it robs house 0, then house n-1 must be excluded — we run House Robber I on indices 0 to n-2. If it does not rob house 0, then house n-1 is free to be robbed or not — we run House Robber I on indices 1 to n-1. One of these two cases contains the optimal answer.
The circular constraint only affects the first and last house — break the circle into two linear problems
House 0 and house n-1 cannot both be robbed. This is the only constraint added by the circular arrangement. Break the circle by running two linear House Robber I passes: one on houses 0..n-2 (excluding the last) and one on houses 1..n-1 (excluding the first). The maximum of the two is the answer to the circular problem.
Two Linear Passes
Case 1 considers houses 0 through n-2 — the full array except the last house. By excluding house n-1 from the range, we guarantee that house 0 and house n-1 are never both available in the same subproblem. Running House Robber I on this range gives the best result when house n-1 is off-limits.
Case 2 considers houses 1 through n-1 — the full array except the first house. By excluding house 0, we similarly guarantee no circular conflict. Running House Robber I on this range gives the best result when house 0 is off-limits.
The final answer is max(case1, case2). Because the optimal solution to the circular problem must either avoid house 0 or avoid house n-1 (or both), one of the two cases will contain the optimal value. We never need a third case or a combined pass — the two linear subproblems are sufficient and exhaustive.
- 1If n == 1: return nums[0] (single house, no adjacency constraint)
- 2Case 1: run robLinear on indices 0 to n-2 (exclude last house)
- 3Case 2: run robLinear on indices 1 to n-1 (exclude first house)
- 4Return max(case1, case2)
- 5robLinear uses two rolling variables: prev2 = best two steps back, prev1 = best one step back
- 6For each house: curr = max(prev1, prev2 + nums[i]); prev2 = prev1; prev1 = curr
- 7Time: O(n), Space: O(1) per pass; O(n) total, O(1) extra space
Why This Covers All Cases
The argument is a clean case split. The optimal solution either includes house 0 or it does not. If it includes house 0, then house n-1 cannot be included (they are adjacent). So the optimal solution lives entirely within houses 0 to n-2 — exactly what case 1 computes. Case 1 returns the maximum possible value when house n-1 is excluded.
If the optimal solution does not include house 0, then the problem is unconstrained with respect to house n-1. The optimal solution lives within houses 1 to n-1 — exactly what case 2 computes. Case 2 returns the maximum possible value when house 0 is excluded. Since one of the two cases must describe the actual optimal solution, taking the max of both cases gives the correct answer.
No other cases exist. The circular constraint is binary: either house 0 is in the optimal solution or it is not. The two-pass approach exhausts both possibilities without overlap or gap. This reasoning pattern — case-split on the element that creates the circular dependency, solve each case linearly — generalizes to other circular DP problems.
You might rob neither house 0 nor house n-1 — both cases still cover this scenario
The case split is on whether house 0 is robbed, not on whether house n-1 is robbed. Case 1 (exclude house n-1) does not force you to rob house 0 — it merely removes house n-1 from the candidate set. If the optimal solution avoids both house 0 and house n-1, it appears in both case 1 and case 2. Either way, max(case1, case2) is correct.
Reusing House Robber I
Each linear pass is exactly the House Robber I recurrence over a contiguous subarray. The rolling-variable implementation uses two variables: prev2 tracks the best result two steps back, and prev1 tracks the best result one step back. At each index i, curr = max(prev1, prev2 + nums[i]). Then shift: prev2 = prev1, prev1 = curr. After iterating through all indices in the range, prev1 holds the answer for that pass.
The two-variable approach avoids allocating an O(n) dp array. Because each position only looks back two steps, there is no need to store earlier values. This is the standard O(1) space optimization for House Robber I, and it applies unchanged to each of the two linear passes in House Robber II.
The total time complexity is O(n) — two linear passes over at most n-1 elements each. The total space complexity is O(1) — only four variables are needed across both passes (prev2, prev1, curr for each pass, reused between passes). This is optimal: you must read all n inputs, and no additional memory is required beyond constant working variables.
- 1def robLinear(nums, start, end):
- 2 prev2, prev1 = 0, 0
- 3 for i in range(start, end + 1):
- 4 curr = max(prev1, prev2 + nums[i])
- 5 prev2, prev1 = prev1, curr
- 6 return prev1
- 7O(n) time per call; O(1) space; called twice for total O(n) / O(1)
Code Walkthrough Python and Java
Python: def rob(nums): n = len(nums); if n == 1: return nums[0]; def robLinear(start, end): prev2, prev1 = 0, 0; [prev2 := prev1, prev1 := max(prev1, prev2 + nums[i]) for i in range(start, end+1)]; return prev1. Cleaner without walrus: def robLinear(start, end): p2, p1 = 0, 0; for i in range(start, end+1): p2, p1 = p1, max(p1, p2+nums[i]); return p1. Main: return max(robLinear(0, n-2), robLinear(1, n-1)). Handles n==1 before calling robLinear to avoid empty ranges.
Java: public int rob(int[] nums) { int n = nums.length; if (n == 1) return nums[0]; return Math.max(robLinear(nums, 0, n-2), robLinear(nums, 1, n-1)); } private int robLinear(int[] nums, int start, int end) { int prev2 = 0, prev1 = 0; for (int i = start; i <= end; i++) { int curr = Math.max(prev1, prev2 + nums[i]); prev2 = prev1; prev1 = curr; } return prev1; }. The helper method is identical in structure to the standalone House Robber I solution.
The n==1 edge case must be handled before calling robLinear because robLinear(0, n-2) with n==1 becomes robLinear(0, -1), which is an empty range returning 0 — incorrect for a single house. Similarly robLinear(1, 0) is empty. Both cases return 0, but the correct answer for a single house is nums[0]. Always guard against n==1 before the two-pass logic.
Handle n=1 separately — with one house there is no circular constraint, just return nums[0]
When n == 1, robLinear(0, n-2) = robLinear(0, -1) iterates over an empty range and returns 0. robLinear(1, n-1) = robLinear(1, 0) is also empty and returns 0. max(0, 0) = 0 is wrong — the answer should be nums[0]. Always check if n == 1 and return nums[0] before executing the two-pass logic. Some implementations also check n == 2 and return max(nums[0], nums[1]) for clarity, but the two-pass logic handles n == 2 correctly without a special case.