Problem overview
Rob receives an int[] nums where nums[i] is the money in house i, and returns the most you can rob without robbing two neighbouring houses. The houses stand in a circle, so the last house is also next to house 0, and the first and last houses can't both be robbed.
For [2,3,2] the answer is 3: houses 0 and 2 hold 2 each but are neighbours in the circle, so you rob house 1. For [1,2,3,1] the answer is 4, from houses 0 and 2 (1 + 3). For [400,0,0,400,0,0,400] the answer is 800. On a street with two ends, houses 0, 3 and 6 would give 1200, but in the circle house 6 touches house 0, so only two of those three can be taken.
Approach: break the circle in two places
On a straight street, the problem is the classic House Robber: walk along the houses keeping two running totals. The circle adds one rule, so reduce it to the straight case.
Any valid plan leaves out the first house or the last house (or both), because they can't both be robbed. So:
- the best plan that never uses the last house is the best on the straight street of houses
0ton - 2; - the best plan that never uses the first house is the best on the straight street of houses
1ton - 1.
The answer is the larger of the two. A plan that uses neither end is counted in both, which is harmless, since we only want the maximum. Each run takes O(n) time, so the whole algorithm does too.
Step by step
- Store the length:
int n = nums.Length. - One house: if
n == 1, returnnums[0]. A single house has no neighbour, so you rob it. The two runs below each leave out one house, and with one house that would leave nothing. - Leave out the last house:
withoutLast = RobRange(nums, 0, n - 2). The helperRobRangesolves the straight street fromstarttoend; the lesson assumes it exists and writes it afterwards. - Leave out the first house:
withoutFirst = RobRange(nums, 1, n - 1). - Return
Math.Max(withoutLast, withoutFirst). - Write
RobRange(int[] nums, int start, int end), apublicmethod returningint. prevstarts at 0: the best total up to two houses before the current one. Before the range begins, nothing has been robbed.currstarts at 0: the best total up to the house just before the current one.- Loop
ifromstartwhilei <= end. It is<=because houseendbelongs to the range. - Decide house
i:best = Math.Max(curr, prev + nums[i]). Either skip the house and keepcurr, or rob it, which means housei - 1stays untouched, so its money is added toprev. - Slide the totals:
prev = currfirst, thencurr = best. The order matters, because changingcurrfirst would overwrite the valueprevneeds. - Return
curr, the best total for housesstarttoend.
The solution in C#
class Solution {
public int Rob(int[] nums) {
int n = nums.Length;
if (n == 1) {
return nums[0];
}
int withoutLast = RobRange(nums, 0, n - 2);
int withoutFirst = RobRange(nums, 1, n - 1);
return Math.Max(withoutLast, withoutFirst);
}
public int RobRange(int[] nums, int start, int end) {
int prev = 0;
int curr = 0;
for (int i = start; i <= end; i++) {
int best = Math.Max(curr, prev + nums[i]);
prev = curr;
curr = best;
}
return curr;
}
}
C# details worth knowing:
- The array is not copied.
RobRangereceives the samenumsand two indexes. Slicing a copy of the array for each run would also work but costs extra memory for nothing. Math.Maxcomes fromSystemand takes two numbers of the same type; here both areint. C# method names are PascalCase, and so areRobandRobRange.- Overflow is not a concern here. The lesson allows up to 100 houses of at most 1000 each, so a total is at most 100,000, far below the
intlimit of 2,147,483,647. In general,intarithmetic wraps silently when it overflows, without an exception. - The loop can be empty. With two houses,
RobRange(nums, 0, 0)andRobRange(nums, 1, 1)each visit one house. Withn == 1the ranges would be0to-1and1to0, which the loop would skip and give 0; that is why the single-house case is handled first.
Complexity
Time: O(n). RobRange visits at most n - 1 houses, and Rob calls it twice, so each house is visited at most twice.
Space: O(1). Only a handful of int variables are used, whatever the number of houses.
Edge cases and common mistakes
- One house.
[5]returns 5. Without then == 1check, both ranges would be empty and the answer would be 0. - Two houses.
[1,2]returns 2: they are neighbours, so only the richer one is robbed. - Running the straight solution on the whole array. For
[2,3,2]it would rob houses 0 and 2 for 4, which the circle forbids. The correct answer is 3. - Off-by-one in the ranges. The first run ends at
n - 2and the second starts at1; both ranges include their last house, which is why the loop uses<=. - Updating
prevandcurrin the wrong order. Settingcurr = bestbeforeprev = currmakes both variables equal and loses the older total.
Testing
Running Rob on a few arrays gives:
nums | Result |
|---|---|
[2,3,2] | 3 |
[1,2,3,1] | 4 |
[400,0,0,400,0,0,400] | 800 |
[5] | 5 |
[1,2] | 2 |
[2,7,9,3,1] | 11 |
For [2,3,2], RobRange(nums, 0, 1) returns 3 and RobRange(nums, 1, 2) returns 3, so Rob returns 3.
