Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
The houses form a circle, so house 0 and house n-1 are now adjacent — you can't rob both. Split into two linear House Robber subproblems: one over houses [0 .. n-2] and one over [1 .. n-1], and return the larger. Each linear pass is the O(1)-space rolling-variable DP, so the whole thing is O(n) time, O(1) space.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you recognize that the only new wrinkle is the 0↔n-1 adjacency, and reduce the circular problem to two calls of the linear solver rather than inventing a fragile circular recurrence.
Where candidates slip
Trying to handle the wrap-around inside a single DP pass (off-by-one nightmares), or forgetting the n == 1 special case where both ranges are empty.
How to stand out
State the reduction crisply — 'fix whether house 0 is robbable, that breaks the cycle into two linear runs' — reuse the O(1) linear solver, and call out the single-house edge case before being asked.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewMax loot in a CIRCLE of housesOnly one house?YesNoExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 3
Brute Force — Recursion With Endpoint Cases
1Hint 1Explore hintHide hint
The only new rule is that houses 0 and n-1 are adjacent. What if you simply forbade one of them up front?
2Hint 2Explore hintHide hint
Case A: pretend house 0 doesn't exist → rob the line [1 .. n-1]. Case B: pretend house n-1 doesn't exist → rob the line [0 .. n-2]. The circle can't beat both.
3Hint 3Explore hintHide hint
For each case, recurse exactly like linear House Robber: rob(i) = max(nums[i] + rob(i+2), rob(i+1)) bounded by that case's range.
A · Core idea
The circular constraint sounds scary, but it collapses to a single observation: in any valid plan, house 0 and house n-1 are never both robbed. So enumerate the two ways to honor that: either we leave house 0 out (and the remaining houses 1 .. n-1 form a straight line) or we leave house n-1 out (houses 0 .. n-2 form a straight line). The optimal circular answer is the better of these two linear answers.
The mental shift: break the cycle by fixing an endpoint. A ring is hard because the two ends wrap into each other. The instant you force one end to be excluded, the wrap-around disappears and you're left with the plain line problem you already know how to solve. Two cases cover every possibility because at least one end must be missing.
Why two cases, not three. You might worry about the plan that robs neither end — but that plan is already a sub-case of both ranges, so it's covered. There's no scenario that escapes both [0 .. n-2] and [1 .. n-1].
Why we don't ship this. Each linear recursion overlaps subproblems exponentially, exactly like House Robber I. Name the reduction, then cache it.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
nums = [1, 2, 3, 1]Case A range [0..2]
linear rob on [1,2,3] → max is rob 0 & 2 = 1+3
caseA = 44C · Code with teaching comments
Reference implementation previewpublic int rob(int[] nums) { int n = nums.length; if (n == 1) { return nums[0]; }Explore the full codeHide the full code
D · Why it works and complexityO(2ⁿ) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The reduction is the whole trick: a closed loop with conflicting ends becomes two open lines, each missing one end. Solving the harder problem by calling the easier one twice is a reusable move — it shows you'd rather compose known solutions than reinvent a brittle special case.
ComplexityO(2ⁿ) time, O(n) space — Each linear recursion branches into two per house → ~2ⁿ nodes, and we run it twice (still O(2ⁿ)). Recursion depth O(n) for the stack.
E · Edge cases and pitfalls4nums = [5] — n == 1 guard returns 5 immediately, avoiding the empty-range reduction.Explore edge casesHide edge cases
Edge cases handled
nums = [5]n == 1 guard returns 5 immediately, avoiding the empty-range reduction.
Pitfalls
- Forgetting the n == 1 guard — both ranges (0..-1 and 1..0) are empty and you'd return 0 instead of nums[0].
- Using ranges that overlap on both ends (e.g. [0..n-1] for both) — reintroduces the 0↔n-1 conflict.
- Shipping the recursion — it's still exponential per range.
Approach 2 of 3
Top-Down Memoization
1Hint 1Explore hintHide hint
Within a single range, rob(i) returns the same value every time. Cache it.
2Hint 2Explore hintHide hint
Run memoized linear House Robber twice — once per range — with a fresh memo for each so indices don't collide between passes.
3Hint 3Explore hintHide hint
memo[i] = max(nums[i] + solve(i+2), solve(i+1)), bounded by the range's hi.
A · Core idea
Keep the two-range reduction, but memoize each linear pass so the exponential recursion becomes linear. For a given range, solve(i) caches the best loot achievable from house i to the range's end; the second time that suffix is needed it's a free read.
The mental shift: memoize the inner problem, not the outer split. The circular reduction is just two independent linear problems. We don't memoize across the split — each pass gets its own cache because index i means different things relative to each range. Inside a pass, the optimization is identical to House Robber I: cache solve(i) once.
Why a fresh cache per range. If both passes shared one memo, an answer computed for range [0 .. n-2] could be wrongly reused for range [1 .. n-1], where the boundaries differ. Resetting the cache (or keying it by range) keeps the two passes correct and independent.
Why the sentinel is -1. Money can be 0, so 0 is a valid cached answer. Use -1 to mark 'not computed' so a genuine zero suffix isn't mistaken for a cache miss.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
nums = [2, 3, 2]A: solve(1)
max(3 + 0, 0) = 3, store
memoA[1] = 33C · Code with teaching comments
Reference implementation previewpublic int rob(int[] nums) { int n = nums.length; if (n == 1) { return nums[0]; }Explore the full codeHide the full code
D · Why it works and complexityO(n) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
Memoization here is layered on top of a reduction: solve the circle by reducing to lines, then make each line fast with a cache. Composing a problem-shape reduction with a standard speed-up is a very common interview move — recognize the sub-problem, then apply the textbook optimization to it.
ComplexityO(n) time, O(n) space — Each range has at most n distinct subproblems solved once in O(1) → O(n) per pass, two passes → O(n) total. O(n) for the memo and recursion stack.
E · Edge cases and pitfalls5nums = [1, 2] — Range A = [0..0] → 1, Range B = [1..1] → 2, max = 2. Two adjacent circular houses, pick the bigger.Explore edge casesHide edge cases
Edge cases handled
nums = [1, 2]Range A = [0..0] → 1, Range B = [1..1] → 2, max = 2. Two adjacent circular houses, pick the bigger.nums = [0, 0, 0]All zeros; the -1 sentinel keeps real zero answers distinct from misses, result 0.
Pitfalls
- Sharing one memo across both ranges — cross-contaminates answers with different boundaries.
- Using 0 as the uncomputed sentinel — collides with legitimate zero loot.
- Dropping the n == 1 guard — empty ranges return 0.
Approach 3 of 3 · Optimal
Bottom-Up — Two Linear Passes, Two Variables Each (Optimal)
1Hint 1Explore hintHide hint
You already have the O(1) rolling-variable linear House Robber. How many times do you need to call it for the circle?
2Hint 2Explore hintHide hint
Call it once on houses [0 .. n-2] and once on [1 .. n-1], then return the max. Don't allocate any DP array.
3Hint 3Explore hintHide hint
Each call keeps just rob1 and rob2; the single-house case returns nums[0] before the split.
A · Core idea
Combine the reduction with the constant-space linear solver. Write a helper robLinear(lo, hi) that runs the familiar two-variable House Robber over an index range, then return max(robLinear(0, n-2), robLinear(1, n-1)). Guard n == 1 up front so neither range is empty.
The mental shift: compose, don't reinvent. The optimal House Robber I is a five-line rolling loop. The circular version needs nothing new algorithmically — it just calls that loop twice over two ranges. Recognizing that the hard problem is two instances of the easy one is the entire interview signal here.
Why this stays O(1) space. Each linear pass keeps only rob1 and rob2; we never materialize a dp[] array, and the two passes run sequentially, reusing the same two variables. So memory is constant regardless of how many houses there are.
Why the single-house guard is non-negotiable. With n == 1, the range [0 .. n-2] is [0 .. -1] and [1 .. n-1] is [1 .. 0] — both empty, so the helper would return 0 and you'd lose the lone house's money. Returning nums[0] before the split fixes it. (The two-house case is fine: ranges [0..0] and [1..1] correctly yield max(nums[0], nums[1]).)
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
nums = [2, 3, 2]Array state
Case A: range [0..2]
drop house 3 — rob a line of [1,2,3]
3C · Code with teaching comments
D · Why it works and complexityO(n) timeO(1) spaceOpen the invariant and proofExplore the proofHide the proof
The constant-space trick from House Robber I carries over untouched — the circle only adds an outer 'try both endpoint exclusions' wrapper. When a follow-up adds a global constraint to a problem you've solved, ask whether you can express the constraint as a small set of restricted re-runs rather than a brand-new algorithm. That reframing turns 'medium' circular problems into two 'easy' linear ones.
ComplexityO(n) time, O(1) space — Two linear scans, each O(n) with O(1) work per house → O(n) time. Each scan keeps two integers → O(1) space overall.
E · Edge cases and pitfalls5nums = [1, 2] — Ranges [0..0] → 1 and [1..1] → 2; max = 2. Adjacent circular pair handled by the split.Explore edge casesHide edge cases
Edge cases handled
nums = [1, 2]Ranges [0..0] → 1 and [1..1] → 2; max = 2. Adjacent circular pair handled by the split.nums = [200, 3, 140, 20, 10]Case A (drop last) lets you rob 200 + 140 = 340; Case B can't pair with 200's neighbor as well. Returns 340.
Pitfalls
- Omitting the n == 1 guard — both ranges empty, returns 0 instead of the single house.
- Building a full dp array per pass — wastes the O(1) space the rolling variables give you.
- Returning rob1 instead of rob2 from robLinear — off-by-one excluding the last house in the range.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic int rob(int[] nums) { int n = nums.length; if (n == 1) { return nums[0]; }Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Ignoring the circular adjacency
Running plain linear House Robber lets you rob both house 0 and house n-1, which the circle forbids. On [2,3,2] it would return 4 instead of 3.
Explore the code comparisonHide the code comparison
Ignoring the circular adjacency
Running plain linear House Robber lets you rob both house 0 and house n-1, which the circle forbids. On [2,3,2] it would return 4 instead of 3.
Don't write this
return rob_linear(0, n - 1)Write this instead
return max(rob_linear(0, n - 2), rob_linear(1, n - 1))2Missing the single-house guard
With n == 1 the ranges [0 .. n-2] and [1 .. n-1] are both empty, so the helper returns 0 and the lone house's money is lost.
Explore the code comparisonHide the code comparison
Missing the single-house guard
With n == 1 the ranges [0 .. n-2] and [1 .. n-1] are both empty, so the helper returns 0 and the lone house's money is lost.
Don't write this
return Math.max(robLinear(nums, 0, n - 2), robLinear(nums, 1, n - 1));Write this instead
if (n == 1) {
return nums[0];
}
return Math.max(robLinear(nums, 0, n - 2), robLinear(nums, 1, n - 1));3Overlapping both endpoints in a range
If either range spans the full [0 .. n-1], the 0↔n-1 conflict comes back. Each range must exclude exactly one endpoint.
Explore the code comparisonHide the code comparison
Overlapping both endpoints in a range
If either range spans the full [0 .. n-1], the 0↔n-1 conflict comes back. Each range must exclude exactly one endpoint.
Don't write this
int a = robLinear(nums, 0, n - 1);
int b = robLinear(nums, 0, n - 1);Write this instead
int a = robLinear(nums, 0, n - 2);
int b = robLinear(nums, 1, n - 1);Revise
Carry the pattern into the interview
Pattern to remember
Circular 1D DP — reduce to two linear runs
Code pattern to memorize
if n == 1: return nums[0]
return max(robLinear(nums, 0, n-2), robLinear(nums, 1, n-1))
# robLinear is the standard rolling-variable House RobberRules to remember
- 1The circle makes house 0 and house n-1 adjacent — they can never both be robbed.
- 2Break the cycle by FIXING the choice on house 0: either it's out (consider [1 .. n-1]) or the last house is out (consider [0 .. n-2]).
- 3Run the linear House Robber on each range and take the max — never write a special circular recurrence.
- 4Single-house input is the trap: with
n == 1both ranges are empty, so special-case it to returnnums[0]. - 5Each linear pass is the same
rob1/rob2rolling-variable DP — O(1) space, reused twice.
Use this pattern when
- House Robber constraints, but the line is closed into a ring
- A circular-array DP where the two endpoints conflict with each other
- You can remove the wrap-around by fixing one endpoint's inclusion
- Any 'cannot pick both ends' constraint that splits cleanly into two open cases
Choose another pattern when
- Houses are a straight line → plain House Robber, no split needed
- Houses form a tree → House Robber III, post-order DP
- More than two endpoints interact → the two-range trick won't cover all cases
- You're counting circular arrangements, not maximizing → a different combinatorial DP
Interview takeaway
House Robber II teaches the canonical 'break the circle' move: a ring constraint where two ends conflict reduces to two linear problems with one end excluded each. Solve the hard variant by reusing the easy one twice — a reduction pattern that recurs all over circular-array DP.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCircular 1D Dynamic Programming — reduce a ring to two linear runs by excluding one conflicting endpoint each. Recognize it whenever two array endpoints interact. Built directly on top of linear House Robber.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- House RobberThe linear base case this problem reduces to — solve it first, then call it twice.
- House Robber IIIComing soonHouses form a binary tree; post-order DP returns (rob, skip) for each node.
- Delete and EarnComing soonBucket by value, then it's linear House Robber on the value axis.
- Circular array max-sum problemsSame 'break the circle into two linear cases' reduction reappears for circular subarray sums.