Start learning free

LESSON 450 OF 541 · DYNAMIC PROGRAMMING

House Robber II in Swift: O(n) Dynamic Programming

Practice O(n) Read the full explanation

You are a robber planning to rob houses that stand in a circle: house 0 comes after the last house, so the first house and the last house are neighbors. Each house has some money stashed, given in an array nums: house i holds nums[i].

Neighboring houses share a security system: if two adjacent houses are robbed on the same night, the police are alerted. Return the maximum amount of money you can rob tonight without alerting the police.

This is House Robber with one change: the street is now a circle, so the first and the last house can't both be robbed.

Example 1:

Input: nums = [2, 3, 2]
Output: 3
Explanation: House 0 and house 2 both hold 2, but they are neighbors in the circle, so they can't both be robbed. Rob house 1 (money 3).

Example 2:

Input: nums = [1, 2, 3, 1]
Output: 4
Explanation: Rob house 0 (money 1) and house 2 (money 3): 1 + 3 = 4.

Example 3:

Input: nums = [400, 0, 0, 400, 0, 0, 400]
Output: 800
Explanation: On a street with two ends, houses 0, 3 and 6 would give 1200. In the circle, house 6 is next to house 0, so only two of those three houses can be robbed, for example houses 0 and 3: 400 + 400 = 800.

Constraints:

  • 1 <= nums.count <= 100
  • 0 <= nums[i] <= 1000

Note:

Your algorithm should run in O(n) time, where n is the number of houses.

STEP 1 OF 12
    Ctrl + Enter to check

    SWIFT · DYNAMIC PROGRAMMING

    House Robber Ii in Swift, explained

    Problem overview

    rob receives an array nums, where nums[i] is the money in house i, and returns the most money that can be robbed without taking from two neighbouring houses. The houses stand in a circle, so the last house is a neighbour of house 0.

    For [2, 3, 2] the answer is 3. Houses 0 and 2 hold 2 each, but they are neighbours in the circle, so only one of them can be taken, and house 1 alone (3) beats either of them. For [1, 2, 3, 1] the answer is 4: houses 0 and 2, giving 1 + 3.

    Approach: break the circle into two streets

    On a street with two ends, the classic solution keeps two running totals as it walks along. At each house, the best total is either the previous best (skip this house) or the best from two houses back plus this house's money (rob it).

    The circle adds one rule: the first and last houses can't both be robbed. So every valid plan leaves out the first house, or the last one, or both. That gives two plain streets to solve: houses 0 to n - 2 (last house left out) and houses 1 to n - 1 (first house left out). The answer is the larger of the two results. A plan that robs neither end appears in both, which does no harm, as only the best total matters.

    The need for this split shows on [2, 3, 2]: running the street solution over the whole array gives 4, which robs both ends and is not allowed in the circle. The two shorter streets, [2, 3] and [3, 2], both give 3.

    Step by step

    1. Count the houses. Store nums.count in n.
    2. One house. If n == 1, return nums[0]. A single house has no neighbour, and both ranges below would be empty.
    3. Leave out the last house. withoutLast is robRange(nums, 0, n - 2). The helper solves the street problem for houses start to end.
    4. Leave out the first house. withoutFirst is robRange(nums, 1, n - 1).
    5. Take the larger with max.
    6. The helper's signature. robRange(_ nums: [Int], _ start: Int, _ end: Int) -> Int, with _ before each name so the calls pass the values without labels.
    7. prev. A variable starting at 0: the best total up to two houses before the current one.
    8. curr. A variable starting at 0: the best total up to the house just before the current one.
    9. Loop i over the closed range start...end.
    10. Decide house i. best is the larger of curr (skip it) and prev + nums[i] (rob it).
    11. Shift the totals. Set prev to curr first, then curr to best. The other order would overwrite the value prev needs.
    12. Return curr, the best total for the whole range.

    The solution in Swift

    class Solution {
    
        func rob(_ nums: [Int]) -> Int {
            let n = nums.count
            if n == 1 {
                return nums[0]
            }
            let withoutLast = robRange(nums, 0, n - 2)
            let withoutFirst = robRange(nums, 1, n - 1)
            return max(withoutLast, withoutFirst)
        }
    
        func robRange(_ nums: [Int], _ start: Int, _ end: Int) -> Int {
            var prev = 0
            var curr = 0
            for i in start...end {
                let best = max(curr, prev + nums[i])
                prev = curr
                curr = best
            }
            return curr
        }
    }
    

    The Swift details worth a closer look:

    • Closed ranges must not run backwards. start...end includes end, and Swift crashes at run time when end is smaller than start. With one house, robRange(nums, 0, -1) would build such a range, which is why rob returns early when n == 1. From two houses on, both calls have start <= end.
    • var and let. prev and curr change in the loop, so they are var. best is computed once per round, so it is let.
    • The global max. max(a, b) is a free function in Swift's standard library, not a method on a number.
    • Unlabelled parameters. Because of the _, robRange(nums, 0, n - 2) needs no start: or end: labels. Without it, the call would not compile.
    • Arrays are passed without copying. nums is a value type, but the helper only reads it, so no copy of the elements is made.

    Complexity

    Time: O(n). The two calls to robRange each walk at most n - 1 houses with constant work per house.

    Space: O(1). The code keeps a few integers and builds no table.

    Edge cases and common mistakes

    • One house. [5] returns 5. Without the early return, the helper would get an empty range that Swift can't build.
    • Two houses. [2, 3] returns 3. The two ranges are each a single house, so the answer is the larger of the two, as the circle requires: they are neighbours.
    • Treating the circle as a street. For [400, 0, 0, 400, 0, 0, 400] the street answer is 1200, which robs both ends. The circle answer is 800.
    • Zeros. [0, 0] returns 0. The money is never negative, so doing nothing is always an option.
    • Updating in the wrong order. Assigning curr = best before prev = curr makes both totals equal and breaks the next round.
    • Using n - 1 as the first call's end. That includes the last house, and the first call would then be able to rob both ends.

    Testing

    • [2, 3, 2] gives 3.
    • [1, 2, 3, 1] gives 4.
    • [400, 0, 0, 400, 0, 0, 400] gives 800.
    • [5] gives 5.
    • [2, 3] and [3, 2] both give 3.
    • [1, 1, 1] gives 1: any two of the three houses are neighbours in the circle.
    • [5, 1, 1, 5] gives 6: the two 5s are neighbours in the circle, so one 5 and one 1.
    Keep going with Swift500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free