Problem overview
The houses stand in a circle. Each holds some money, given in the array nums, and two neighboring houses can't both be robbed. Because the street is a circle, house 0 and the last house are neighbors. rob(nums) returns the most money you can take.
For nums = [2, 3, 2] the answer is 3: the two houses with 2 are neighbors in the circle, so you rob the middle one. 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. On a street with two ends the three houses worth 400 would give 1200, but in a circle the first and last are neighbors, so only two of them can be taken.
Approach
On a street with two ends, the problem has a standard running-totals solution: for each house, the best total is either the best total without it, or its money plus the best total two houses back. The circle adds one rule: the first and last house can't both be robbed.
That rule gives a neat split. Any valid plan leaves out the first house or the last house (or both). So:
- Plans that leave out the last house are plans on houses
0ton - 2, a plain street. - Plans that leave out the first house are plans on houses
1ton - 1, also a plain street.
Solve both streets, and take the larger result. A plan that robs neither end appears in both, which does no harm, since we only want the best total. The circle has become two ordinary problems, each solved in one pass.
Step by step
The circle, rob
- Count the houses. Store
nums.sizeinval n. - One house. If
n == 1, returnnums[0]. A lone house has no neighbor, so you take it. This case must come first: both ranges below would leave that house out and be empty. - Without the last house.
val withoutLast = robRange(nums, 0, n - 2). - Without the first house.
val withoutFirst = robRange(nums, 1, n - 1). - Take the better one with
maxOf(withoutLast, withoutFirst).
The street, robRange
- Two totals.
var prev = 0is the best total up to two houses before the current one, andvar curr = 0the best total up to the house just before it. Both start at 0, since nothing has been robbed before the first house of the range. - Loop with
ioverstart..end. This range includes both ends, so houseendis visited. - Decide house
i.val best = maxOf(curr, prev + nums[i]): either skip houseiand keepcurr, or rob it, which requires leaving housei - 1alone, so its money goes on top ofprev. - Shift. Set
prev = currfirst, thencurr = best. Doing it the other way round would overwrite the value thatprevneeds. - After the loop,
curris the best total for the whole range. Return it.
The solution in Kotlin
class Solution {
fun rob(nums: IntArray): Int {
val n = nums.size
if (n == 1) {
return nums[0]
}
val withoutLast = robRange(nums, 0, n - 2)
val withoutFirst = robRange(nums, 1, n - 1)
return maxOf(withoutLast, withoutFirst)
}
fun robRange(nums: IntArray, start: Int, end: Int): Int {
var prev = 0
var curr = 0
for (i in start..end) {
val best = maxOf(curr, prev + nums[i])
prev = curr
curr = best
}
return curr
}
}
Kotlin details worth knowing
start..endis inclusive. Both ends are visited. Whenendis smaller thanstart, the range is empty and the loop body never runs:robRange(intArrayOf(7, 9), 1, 0)returns 0.maxOf.maxOf(a, b)is a standard-library function for two values. There is no need for anifor an import.valinside the loop.bestis a newvalin every round, because it never changes after it is set. The two running totals arevar, because they change each round.- Arrays are passed by reference.
robRangereceives the sameIntArrayand only reads from it. Nothing is copied, even though the helper looks at "a part" of the array: the two integersstartandendsay which part. - Both methods are members of the class.
robcallsrobRangeby its plain name, and the order of the two functions in the class doesn't matter in Kotlin.
Complexity
- Time: O(n). Each of the two calls to
robRangeloops over at most n - 1 houses with constant work per house, so the total is about 2n steps. - Space: O(1). Only a few
Intvariables, no table: the two running totals replace an array of best totals.
Edge cases and common mistakes
- One house.
[5]gives 5. Without then == 1check the range0..-1would be empty and the answer would wrongly be 0. - Two houses.
[1, 2]gives 2: the houses are neighbors, so you take the richer one. The two ranges are each a single house, worth 1 and 2. - Solving it as a plain street. The answer for
[400, 0, 0, 400, 0, 0, 400]on a street is 1200, but the circle's answer is 800. Ignoring the link between the ends gives the wrong number whenever both ends are used. - Including the wrong ends. The two ranges are
0..n - 2and1..n - 1. Using0..n - 1for one of them brings back the street answer. - Overwriting
prevtoo early. Settingcurr = bestbeforeprev = currloses the oldcurr. - All zeros. The answer is 0, not negative: the totals start at 0 and the values are never negative.
Testing
These results come from running the solution above, with the two ranges' results shown to see which one wins:
nums | Without last | Without first | rob |
|---|---|---|---|
[2, 3, 2] | 3 | 3 | 3 |
[1, 2, 3, 1] | 4 | 3 | 4 |
[400, 0, 0, 400, 0, 0, 400] | 800 | 800 | 800 |
[1, 2] | 1 | 2 | 2 |
[2, 1, 1, 2] | 3 | 3 | 3 |
[5] | not used | not used | 5 |
