Problem overview
rob receives an Array[Int], nums, where nums(i) is the money in house i, and returns the most money you can take without robbing two neighbouring houses. The houses stand in a circle, so the last house is also next to house 0, and those two 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 the best plan is house 1 alone. For [1, 2, 3, 1] the answer is 4, from houses 0 and 2. For [400, 0, 0, 400, 0, 0, 400] the answer is 800. Houses 0, 3 and 6 would give 1200 on a street with two ends, and the same array run as a plain street does give 1200, but in the circle houses 6 and 0 touch, so only two of the three can be taken.
Approach: turn the circle into two streets
On a street with two ends, the problem is the classic House Robber. Walking from left to right, the best total up to house i is either the best total up to house i - 1 (skip house i) or the best total up to house i - 2 plus nums(i) (rob house i, which forces house i - 1 to be skipped). Only the last two totals are needed, so two variables are enough.
The circle adds one rule: the first and the last house can't both be robbed. So every valid plan leaves out the first house, the last house, or both. That gives two cases, and each one is an ordinary street:
- leave out the last house, and solve for houses 0 to
n - 2; - leave out the first house, and solve for houses 1 to
n - 1.
A plan that leaves out both ends fits both streets, which does no harm: the answer is the larger of the two results. Each street takes one pass, so the total is O(n).
Step by step
- Store the length in
val n = nums.length. - Handle one house. If
n == 1, returnnums(0). A single house has no neighbour, and each of the two streets below would be empty. - Leave out the last house.
val withoutLast = robRange(nums, 0, n - 2), assuming the helper exists for now. - Leave out the first house.
val withoutFirst = robRange(nums, 1, n - 1). - Return the larger with
math.max(withoutLast, withoutFirst)as the last expression. - Write the helper's header.
robRangetakesnums,startandendand returns anInt: the best total for housesstarttoendon a street with two ends. var prev = 0: the best total up to two houses before the current one.var curr = 0: the best total up to the house just before the current one. Before the first house of the range nothing has been robbed, so both start at 0.- Loop with
i <- start to end. The rangetoincludesend. - Compute the best total up to house
i:val best = math.max(curr, prev + nums(i)). - Shift the totals. Set
prev = currfirst, thencurr = best. The order matters: changingcurrfirst would overwrite the valueprevneeds. - Return
currafter the loop, as the last expression.
The solution in Scala
class Solution {
def rob(nums: Array[Int]): Int = {
val n = nums.length
if (n == 1) {
return nums(0)
}
val withoutLast = robRange(nums, 0, n - 2)
val withoutFirst = robRange(nums, 1, n - 1)
math.max(withoutLast, withoutFirst)
}
def robRange(nums: Array[Int], start: Int, end: Int): Int = {
var prev = 0
var curr = 0
for (i <- start to end) {
val best = math.max(curr, prev + nums(i))
prev = curr
curr = best
}
curr
}
}
Here is robRange on the houses 0 to 1 of [2, 3, 2] (the case without the last house), as the loop runs:
i=0 prev=0 curr=0 best=2
i=1 prev=0 curr=2 best=3
The result is 3. And on houses 0 to 2 of [1, 2, 3, 1]:
i=0 prev=0 curr=0 best=1
i=1 prev=0 curr=1 best=2
i=2 prev=1 curr=2 best=4
That gives 4, which is the answer for that array.
Scala details worth knowing:
- The last expression is the result.
math.max(withoutLast, withoutFirst)andcurrhave noreturnin front of them. The earlyreturn nums(0)inside theifleaves the method sooner, which is the one place the code needs it. toversusuntil.start to endincludesend, whilestart until endstops before it. Whenendis smaller thanstart, the range is empty and the loop doesn't run:robRange(Array(5, 6), 1, 0)returns 0.valandvar.prevandcurrarevarbecause they change on every round.bestis aval, created fresh inside each round of the loop.- Ints are enough. With at most 100 houses of at most 1000 each, the total can't pass 100,000, far below the 32-bit limit.
- Method order.
robcan callrobRangealthough it is defined below it. Methods of a class see each other in any order.
Complexity
Time: O(n). robRange runs once for n - 1 houses and once more for n - 1 houses, each with constant work per house, so each house is visited at most twice.
Space: O(1). Only n, the two results and the two running totals are stored.
Edge cases and common mistakes
- A single house.
[5]gives 5. Without then == 1check, the two streets would be empty and the answer would be 0. - Two houses. They are neighbours, so only one can be robbed:
[1, 2]gives 2. Each street has one house (houses 0 to 0 and 1 to 1), and the larger one wins. - All zeros.
[0, 0]gives 0. Nothing special is needed;0is the right starting total. - Solving it as a street. Running the street version on the whole array gives 1200 for
[400, 0, 0, 400, 0, 0, 400], which robs two neighbouring houses of the circle. The correct answer is 800. - Dropping both ends. Skipping house 0 and the last house in one run can miss the best plan. In
[1, 2, 3]the best is house 2 alone for 3, and a run over house 1 only would give 2. - Updating in the wrong order. Writing
curr = bestbeforeprev = currmakesprevequalbesttoo, and the totals stop being two houses apart. - Off-by-one on the ranges. The two ranges are 0 to
n - 2and 1 ton - 1, both inclusive. Usinguntilwith these values would drop a house.
Testing
Results from running the solution:
nums | Without the last house | Without the first house | rob |
|---|---|---|---|
[2, 3, 2] | 3 | 3 | 3 |
[1, 2, 3, 1] | 4 | 3 | 4 |
[400, 0, 0, 400, 0, 0, 400] | 800 | 800 | 800 |
[5] | not used | not used | 5 |
[1, 2] | 1 | 2 | 2 |
[1, 2, 3] | 2 | 3 | 3 |
[1000, 1, 1, 1000] | 1001 | 1001 | 1001 |
