What changes from Part 1?
The houses now form a circle. House 0 and house n−1 are adjacent neighbors. You still can't rob two adjacent houses.
This one extra constraint breaks the linear DP from Part 1 — you can no longer rob both ends.
Key trick: If you can't have both house 0 and house n−1, then the answer must come from one of two scenarios — either you exclude house 0 or you exclude house n−1. Run linear House Robber on each range, take the max.
Algorithm
Pass 1: rob(nums[0 .. n−2])
// exclude last house
Pass 2: rob(nums[1 .. n−1])
// exclude first house
return max(Pass1, Pass2)
// Base case: n==1 → return nums[0]
How the array is split for a circle of n houses
House 0 is eligible · House n−1 is excluded
Finds best loot from the first n−1 houses
House 0 is excluded · House n−1 is eligible
Finds best loot from the last n−1 houses
Complexity
Two linear passes of O(n) each — still just one scan effectively.
Two variables (prev2, prev1) per pass. No dp array needed.
Exact same logic as Part 1, run twice on overlapping sub-arrays.
Examples & edge cases
Step-by-step DP table
| Step | Pass | House i | nums[i] | Rob | Skip | dp[i] | Decision |
|---|
C++ implementation
#include <bits/stdc++.h> using namespace std; class Solution { private: // Linear House Robber on subarray [lo, hi] inclusive int robLinear(vector<int>& nums, int lo, int hi) { if (lo == hi) return nums[lo]; int prev2 = nums[lo]; int prev1 = max(nums[lo], nums[lo + 1]); for (int i = lo + 2; i <= hi; i++) { int cur = max(prev2 + nums[i], prev1); prev2 = prev1; prev1 = cur; } return prev1; } public: int rob(vector<int>& nums) { int n = nums.size(); if (n == 1) return nums[0]; if (n == 2) return max(nums[0], nums[1]); // Pass 1: exclude last house → rob range [0, n-2] int pass1 = robLinear(nums, 0, n - 2); // Pass 2: exclude first house → rob range [1, n-1] int pass2 = robLinear(nums, 1, n - 1); return max(pass1, pass2); } };
My Notes