This is a direct extension of the classic House Robber problem, but with a twist: the houses are arranged in a circle instead of a straight line. That circular arrangement means the first and last houses are neighbors, so you cannot rob both of them.
If the houses were in a line, the standard House Robber DP would apply directly. The circular constraint adds a dependency between the first and last elements that a single linear pass cannot handle. To reuse the linear approach, we need to break that circular dependency.
Since house 0 and house n-1 are adjacent, they can never both be robbed. The optimal solution therefore excludes house 0, excludes house n-1, or excludes both. The "both excluded" case is already contained in each of the first two, so two scenarios suffice: split the problem into two linear subproblems, solve each, and take the maximum.
1 <= nums.length <= 100 --> Values are summed, not multiplied, and the maximum total is at most 100 x 1000 = 100,000, which fits comfortably in a 32-bit integer. No overflow handling is needed.0 <= nums[i] <= 1000 --> Values can be zero, and none are negative. Without negative values, robbing a house never reduces the total, which is why the DP only ever compares "take" against "skip".Try every valid combination. At each house, either rob it or skip it, subject to two rules: no two adjacent houses, and not both the first and last house.
Recursion expresses this directly. A function takes a range of houses and returns the maximum money obtainable from it. At each house it makes the take-or-skip decision:
The circular constraint is handled by running this recursion twice: once over houses 0 through n-2 (excluding the last house), and once over houses 1 through n-1 (excluding the first house). The answer is the maximum of the two results.
rob(nums, i) that returns the maximum amount you can rob from houses 0 through i.i < 0, return 0. If i == 0, return nums[0].max(rob(nums, i - 1), nums[i] + rob(nums, i - 2)).nums[0..n-2] and nums[1..n-1].Input:
Case 1 (exclude last house): solve on houses 0 through 1. robLinear(0, 1) compares skipping house 1 (which gives robLinear(0, 0) = 2) against taking house 1 (nums[1] + robLinear(0, -1) = 3 + 0 = 3). The max is 3.
Case 2 (exclude first house): solve on houses 1 through 2. robLinear(1, 2) compares skipping house 2 (robLinear(1, 1) = 3) against taking house 2 (nums[2] + robLinear(1, 0) = 2 + 0 = 2). The max is 3.
The final answer is max(3, 3) = 3.
The same subproblems are recomputed across the recursion tree. Caching each result the first time it is computed removes that redundant work.
The brute force recursion is slow because it revisits the same states repeatedly. A cache (memoization) stores the result for each subproblem and returns it on later calls, turning the exponential recursion into a linear-time algorithm.
The logic is identical to Approach 1: split the circular problem into two linear subproblems, then solve each with the take-or-skip recursion. The difference is a cache that remembers results already computed. The cache is keyed by the end index, which is enough because start is fixed within a single call, so each subproblem is uniquely identified by where it ends.
robMemo(nums, start, end, memo).end is already in memo. If so, return it.max(robMemo(nums, start, end - 1, memo), nums[end] + robMemo(nums, start, end - 2, memo)).memo and return it.Input:
The memoized recursion computes each subproblem once and caches it by end.
Case 1 (houses 0 through 2): robMemo(end=2) needs robMemo(end=1) and robMemo(end=0). The base case gives end=0 -> nums[0] = 1. Then end=1 -> max(1, nums[1]+0) = max(1, 2) = 2, cached. Then end=2 -> max(2, nums[2]+1) = max(2, 4) = 4, cached. Result: 4.
Case 2 (houses 1 through 3): end=1 -> nums[1] = 2. end=2 -> max(2, nums[2]+0) = max(2, 3) = 3, cached. end=3 -> max(3, nums[3]+2) = max(3, 3) = 3, cached. Result: 3.
The final answer is max(4, 3) = 4.
The subproblems have a left-to-right order, so they can be filled iteratively from the smallest index upward, removing the recursion stack entirely.
Instead of solving subproblems top-down with recursion, we can solve them bottom-up by iterating from the first house to the last. At each house i, we compute the maximum amount we can rob from all houses up to i. The recurrence is the same: either rob house i and add the best from i - 2, or skip house i and carry forward the best from i - 1.
We still handle the circular constraint by running this DP twice: once on nums[0..n-2] and once on nums[1..n-1].
robLinear(nums, start, end) that solves the linear House Robber problem on a subarray.dp[i] = max money from houses start through start + i.dp[i] = max(dp[i-1], nums[start + i] + dp[i-2]).max(robLinear(nums, 0, n-2), robLinear(nums, 1, n-1)).The DP array uses O(n) space, but each step reads only the previous two values. Those two values can be held in two variables, dropping the space to O(1).
In the DP recurrence dp[i] = max(dp[i-1], nums[i] + dp[i-2]), each state depends on only the previous two states. Instead of a full array, two variables suffice: prev1 (the best from one house back) and prev2 (the best from two houses back), updated in a rolling fashion as the loop advances.
This is the same optimization used for the Fibonacci sequence, where keeping the last two values replaces storing the whole array.
Two claims need justification here. The first is that two variables are enough. Because dp[i] reads only dp[i-1] and dp[i-2], once dp[i] is computed nothing earlier than dp[i-1] is ever read again, so a sliding window of size 2 loses no information.
The second is that two linear scans cover the circular problem. In any valid plan, house 0 and house n-1 cannot both be robbed. Every plan therefore falls into at least one of two groups: it skips house n-1, or it skips house 0. Case 1 finds the best plan among those that skip house n-1 (houses 0 through n-2). Case 2 finds the best among those that skip house 0 (houses 1 through n-1). A plan that skips both lands in both groups, so no valid plan is missed, and the maximum over the two cases is the global optimum.
robLinear(nums, start, end) using two variables instead of an array.prev2 = 0 (best from two houses back) and prev1 = 0 (best from one house back).start to end: compute current = max(prev1, nums[i] + prev2), then shift: prev2 = prev1, prev1 = current.max(robLinear(nums, 0, n-2), robLinear(nums, 1, n-1)).prev1 and prev2) regardless of the input size.