House Robber II – Solution & Complexity
Solution Walkthrough
Step 1: Why the circle matters
- In the linear House Robber you may take any non-adjacent subset.
- Here house
0and housen-1are neighbors too, so you can never rob both of them.
+----------- wrap-around edge -----------+ | | house 0 -- house 1 -- ... -- house n-2 -- house n-1- That single extra edge is the only difference from the linear problem.
- A single house is a special case: it has no neighbor, so just return it.
Step 2: Split into two linear problems
- Every valid circular plan leaves out house
0, leaves out housen-1, or leaves out both. - So consider two linear ranges:
nums = [ 1, 2, 3, 1 ] index 0 1 2 3 exclude last : [1, 2, 3] houses 0..n-2 exclude first : [2, 3, 1] houses 1..n-1- A plan that skips house
n-1is a valid linear plan on0..n-2; a plan that skips house0is valid on1..n-1; a plan that skips both endpoints fits in either range, so nothing is lost. - Conversely, each range omits one endpoint, so no plan chosen inside a range can use the wrap-around edge. Every candidate is a legal circular plan.
- Therefore the answer is
max(robLinear(0..n-2), robLinear(1..n-1)). - Here is a first version that fills an explicit DP array for each range. Copying the ranges is fine here because the DP array already costs
O(n)space.
- Every valid circular plan leaves out house
Step 3: Collapse the linear pass to O(1) space
- The linear recurrence
best[i] = max(best[i-1], best[i-2] + arr[i])only needs the previous two results. - Replace the DP array with two rolling variables and reuse that helper on both ranges.
- The linear recurrence
Step 4: Optimal two-pass solution
robRange(lo, hi)scans the inclusive index rangelo..hiof the original array, keeping onlyprevOneandprevTwo.- Call it on
0..n-2(exclude last) and1..n-1(exclude first), then take the maximum. Handle the length-1 street first: with one house both ranges are empty and would return0instead of the house. - Pass index bounds rather than slicing: in Python (
nums[1:]), JavaScript (nums.slice(1)) and Ruby (nums[1..]) a slice copiesO(n)elements. Go slices and Rust&nums[1..]are views over the same array, so they stayO(1)extra space.
Step 5: Dry run
Trace
nums = [1,2,3,1].- Range
[1,2,3](drop the last house): best loot is4by robbing1and3. - Range
[2,3,1](drop the first house): best loot is3(either3alone or2 + 1). - Answer is
max(4, 3) = 4.
- Range
Step 6: Common mistakes and follow-ups
- Forgetting the length-1 case: both ranges are empty and the function returns
0instead of the single house. - Slicing the input in Python, JavaScript or Ruby and still claiming
O(1)extra space; those slices copy the array. Pass index bounds instead. - Trying to write one circular recurrence with a flag; the two-pass reduction is simpler and less error-prone.
- Double-counting by allowing both house
0and housen-1. - Follow-up: how would you also return which houses were robbed, not just the total?
- Forgetting the length-1 case: both ranges are empty and the function returns
Step 7: Edge cases to test mentally
- One house: return it directly.
- Two houses: they are adjacent on both sides, so take the larger.
- The wrap neighbor can make a locally greedy pick wrong, so always compare both ranges.
Step 8: Final full solution and complexity
Reduce the circle to two linear passes over index ranges
0..n-2and1..n-1, keeping only rolling state in each. Time isO(n)and extra space isO(1): every language either passes index bounds (Python, JavaScript, Java, C++, Ruby) or a non-copying slice view (Go, Rust).