225. House Robber II
Problem
Return the maximum nonadjacent sum of nonnegative house values arranged in a circle, where first and last positions are adjacent. Empty input returns zero and a singleton may be chosen once. Do not mutate nums.
Examples
Example 1
rob_circle([2,3,2])
=> 3The two endpoint houses conflict around the circle.
Example 2
rob_circle([1,2,3,1])
=> 4Positions 0 and 2 form the best legal selection.
Example 3
rob_circle([7])
=> 7The only house can be selected once.
Reveal the worked solution and reasoning
Why this mechanism works
A legal circular selection cannot contain both endpoints. Therefore it belongs to at least one of two linear cases: exclude the last house, or exclude the first. Solve each range using the previous kata's take/skip recurrence and choose the larger result. A singleton needs special handling because both ranges would otherwise be empty.
State and boundaries
linear(left,right) returns the optimum for the half-open noncircular range. The two calls cover all legal circular selections.
Protocol
- Handle zero and one house.
- Solve [0,n-1) with rolling take/skip state.
- Solve [1,n) the same way.
- Return the larger result.
Visualization
[2,3,2] around a circle
exclude last -> [2,3] best 3
exclude first -> [3,2] best 3
choosing both 2s is forbiddenPython implementation
def rob_circle(nums):
if not nums:
return 0
if len(nums) == 1:
return nums[0]
def linear(left, right):
previous = current = 0
for i in range(left, right):
previous, current = current, max(current, previous + nums[i])
return current
return max(linear(0, len(nums) - 1), linear(1, len(nums)))
Worked execution
rob_circle([2,3,2])
L2 rob_circle: if not nums: nums=[2, 3, 2] L7 linear: previous = current = 0 left=0; right=2; previous=0; current=0; nums=[2, 3, 2] L8 linear: for i in range(left, right): left=0; right=2; previous=0; current=0; nums=[2, 3, 2]; i=0 L9 linear: previous, current = current, max(current, previous + nums[i]) left=0; right=2; previous=0; current=2; nums=[2, 3, 2]; i=0 L8 linear: for i in range(left, right): left=0; right=2; previous=0; current=2; nums=[2, 3, 2]; i=1 L9 linear: previous, current = current, max(current, previous + nums[i]) left=0; right=2; previous=2; current=3; nums=[2, 3, 2]; i=1 L7 linear: previous = current = 0 left=1; right=3; previous=0; current=0; nums=[2, 3, 2] L8 linear: for i in range(left, right): left=1; right=3; previous=0; current=0; nums=[2, 3, 2]; i=1 L9 linear: previous, current = current, max(current, previous + nums[i]) left=1; right=3; previous=0; current=3; nums=[2, 3, 2]; i=1 L8 linear: for i in range(left, right): left=1; right=3; previous=0; current=3; nums=[2, 3, 2]; i=2 L9 linear: previous, current = current, max(current, previous + nums[i]) left=1; right=3; previous=3; current=3; nums=[2, 3, 2]; i=2
Result: 3. The two endpoint houses conflict around the circle.
rob_circle([1,2,3,1])
L2 rob_circle: if not nums: nums=[1, 2, 3, 1] L7 linear: previous = current = 0 left=0; right=3; previous=0; current=0; nums=[1, 2, 3, 1] L8 linear: for i in range(left, right): left=0; right=3; previous=0; current=0; nums=[1, 2, 3, 1]; i=0 L9 linear: previous, current = current, max(current, previous + nums[i]) left=0; right=3; previous=0; current=1; nums=[1, 2, 3, 1]; i=0 L8 linear: for i in range(left, right): left=0; right=3; previous=0; current=1; nums=[1, 2, 3, 1]; i=1 L9 linear: previous, current = current, max(current, previous + nums[i]) left=0; right=3; previous=1; current=2; nums=[1, 2, 3, 1]; i=1 L8 linear: for i in range(left, right): left=0; right=3; previous=1; current=2; nums=[1, 2, 3, 1]; i=2 L9 linear: previous, current = current, max(current, previous + nums[i]) left=0; right=3; previous=2; current=4; nums=[1, 2, 3, 1]; i=2 ... 3 intermediate state changes omitted ... L8 linear: for i in range(left, right): left=1; right=4; previous=0; current=2; nums=[1, 2, 3, 1]; i=2 L9 linear: previous, current = current, max(current, previous + nums[i]) left=1; right=4; previous=2; current=3; nums=[1, 2, 3, 1]; i=2 L8 linear: for i in range(left, right): left=1; right=4; previous=2; current=3; nums=[1, 2, 3, 1]; i=3 L9 linear: previous, current = current, max(current, previous + nums[i]) left=1; right=4; previous=3; current=3; nums=[1, 2, 3, 1]; i=3
Result: 4. Positions 0 and 2 form the best legal selection.
Why the solution is correct
Every circular solution excludes at least one endpoint and is therefore legal in one of the two ranges. Conversely, every nonadjacent selection inside either range is legal on the circle because the excluded endpoint breaks the wraparound conflict. Taking the larger linear optimum is thus exact.
Time and space
O(n) time over two scans and O(1) auxiliary state; index ranges avoid allocating slices.
Common mistakes
Running the linear algorithm once can select both endpoints. Treating a singleton as two mutually adjacent distinct houses wrongly removes its value.
Reasoning card
State: linear(left,right) returns the optimum for the half-open noncircular range. The two calls cover all legal circular selections.
Invariant: Every valid circular solution belongs to at least one case.
Decision to rehearse: Split into two linear cases: exclude first or exclude last.
Finish: Return the larger result
Transfer exercise
Return a selected index set from the winning linear case, translating its indices back to the original circle.