Problem overview
The method rob receives a vector nums, where nums[i] is the money in house i. The houses stand in a circle, so house 0 comes right after the last house and the two are neighbours. Robbing two neighbouring houses on the same night alerts the police. The method returns the largest amount that can be robbed without that.
This is the classic House Robber with one change: on a street with two ends, the first and last house are not neighbours. In a circle they are, so they can't both be robbed.
For nums = [2, 3, 2] the answer is 3. Houses 0 and 2 hold 2 each, but they are neighbours in the circle, so robbing both is not allowed. Robbing house 1 gives 3. For nums = [1, 2, 3, 1] the answer is 4, from houses 0 and 2: 1 + 3.
Approach: break the circle into two streets
The circle has one extra rule: the first and the last house are never both robbed. So every valid plan leaves out the first house or the last house (or both). That suggests two cases:
- Leave out the last house. The houses 0 to
n - 2form a street with two ends, which is the ordinary House Robber. - Leave out the first house. The houses 1 to
n - 1form another ordinary street.
The answer is the larger result of the two cases. A plan that robs neither end appears in both cases, which is harmless, because only the best total matters. No valid plan is missed: whichever end it skips, one of the two runs includes it.
Each street is solved with the usual two running totals. Looking at one house at a time, the best total so far is either the best without this house (skip it) or the money in this house plus the best up to two houses back (rob it, which forces the previous house to stay untouched). Only the last two best totals are needed, so the memory stays constant.
For nums = [400, 0, 0, 400, 0, 0, 400], a straight street would let you rob houses 0, 3 and 6 for 1200. In the circle, houses 6 and 0 are neighbours. The run over houses 0 to 5 gives 800, the run over houses 1 to 6 gives 800, and the answer is 800.
Step by step
- Store the number of houses in
int n = nums.size(). - Handle one house. If
n == 1, returnnums[0]. A single house has no neighbour, so it is robbed. The two cases below each drop a house, and with one house nothing would be left. - Leave out the last house. Store
robRange(nums, 0, n - 2)inwithoutLast. At this point the helper is assumed to exist: it solves House Robber for the houses fromstarttoend. Writing it afterwards keepsrobshort. - Leave out the first house. Store
robRange(nums, 1, n - 1)inwithoutFirst. - Return the larger one with
max(withoutLast, withoutFirst). - Declare the helper.
robRangetakesvector<int>& numsand two ints,startandend, and returns anint. - Create
prev, starting at 0. It is the best total from the houses of the range up to two houses before the current one. Before the first house of the range, nothing has been robbed. - Create
curr, also starting at 0. It is the best total up to the house just before the current one. - Loop over the range with
ifromstartwhilei <= end. The comparison is<=, because houseendbelongs to the range. - Compute the best total up to house
i:int best = max(curr, prev + nums[i]). Either skip the house and keepcurr, or rob it and add its money toprev. - Shift both totals right:
prev = curr;and thencurr = best;. The order matters: changingcurrfirst would lose the valueprevneeds. - Return
currafter the loop. It is the best total for the whole range.
The solution in C++
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if (n == 1) {
return nums[0];
}
int withoutLast = robRange(nums, 0, n - 2);
int withoutFirst = robRange(nums, 1, n - 1);
return max(withoutLast, withoutFirst);
}
int robRange(vector<int>& nums, int start, int end) {
int prev = 0;
int curr = 0;
for (int i = start; i <= end; i++) {
int best = max(curr, prev + nums[i]);
prev = curr;
curr = best;
}
return curr;
}
};
The two runs on the examples give these results, as robRange over houses 0 to n - 2 and then over houses 1 to n - 1:
[2, 3, 2] 3 and 3 -> 3
[1, 2, 3, 1] 4 and 3 -> 4
[400, 0, 0, 400, 0, 0, 400] 800 and 800 -> 800
The C++ details worth a closer look:
- Passing by reference.
vector<int>& numshands the helper the vector itself. Calling it twice copies nothing. - Ranges are inclusive.
robRange(nums, 0, n - 2)includes both house 0 and housen - 2. That is why the loop condition isi <= end. - Member functions can call each other in any order. Inside a class body,
robmay callrobRangeeven though it is written below. - A signed
n.nums.size()is unsigned, butnis anint, son - 2is an ordinary integer. Withnof 2, the first run covers the range 0 to 0 and the second covers 1 to 1. Ifn - 2were computed on the unsigned size with an empty vector, it would wrap around, but the constraints guarantee at least one house. maxneeds matching types. Both arguments ofmaxhere areint. Mixingintandlong longin one call would not compile without a cast.
Complexity
Time: O(n). Each call of robRange goes through at most n - 1 houses with constant work per house. Two calls visit each house at most twice.
Space: O(1). The code keeps a few integers (prev, curr, best and the two results), whatever the number of houses.
Edge cases and common mistakes
- One house.
[5]returns 5. Without then == 1check, both calls would use an empty or reversed range, and the answer would be 0. - Two houses. They are neighbours in both directions, so only one can be robbed.
[2, 7]returns 7: the first run covers house 0 alone and the second covers house 1 alone. - Three equal houses.
[1, 1, 1]returns 1. On a straight street the answer would be 2, but houses 0 and 2 are neighbours here. - Zeros.
[0, 0]returns 0. The values may be 0, so a plan that robs nothing is valid. - Ignoring the circle. Running the plain House Robber over all houses answers 1200 for the third example. The correct answer is 800.
- Dropping the wrong houses. Both ranges must differ by exactly one end. Using
0 .. n - 2twice, or0 .. n - 1in one of the runs, leaves the circle unbroken or loses some plans. - Updating in the wrong order. Writing
curr = bestbeforeprev = currsetsprevto the new total, so a house could be added to the total that already includes its neighbour.
Testing
nums = [2, 3, 2]gives3.nums = [1, 2, 3, 1]gives4.nums = [400, 0, 0, 400, 0, 0, 400]gives800.nums = [5]gives5.nums = [2, 7]gives7.nums = [1, 1, 1]gives1.nums = [4, 1, 1, 4]gives5.nums = [0, 0]gives0.
