Start learning free

LESSON 452 OF 543 · DYNAMIC PROGRAMMING

House Robber II in Rust: O(n) Dynamic Programming on a Circle

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.len() <= 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

    RUST · DYNAMIC PROGRAMMING

    House Robber Ii in Rust, explained

    Problem overview

    rob receives a vector nums, where nums[i] is the money in house i, and returns the most money you can take without robbing two neighbouring houses. This is House Robber with one change: the houses stand in a circle, so the last house is a neighbour of the first.

    For [2, 3, 2] the answer is 3. Houses 0 and 2 hold 2 each, but they are neighbours in the circle, so the best plan is house 1 alone. For [1, 2, 3, 1] it is 4, from houses 0 and 2. For [400, 0, 0, 400, 0, 0, 400] it is 800: on a street with two ends, houses 0, 3 and 6 would give 1200, but houses 6 and 0 are neighbours here, so only two of the three can be used.

    Approach: break the circle into two streets

    On a straight street, the classic solution keeps two running totals. Going house by house, the best total up to house i is the larger of "skip this house" (the previous best) and "rob this house" (its money plus the best from two houses back).

    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 street problems:

    • houses 0 to n - 2 (the last house left out), and
    • houses 1 to n - 1 (the first house left out).

    Neither range contains both ends, so neither has a problem with the circle, and each one is the straight-street problem. The answer is the larger of the two results. A plan that robs neither end appears in both, which does no harm: only the best total counts.

    One house is a special case, because the two ranges would be empty or invalid. With a single house, you rob it.

    Step by step

    1. Store nums.len() in n. It is a usize, the type Rust uses for indexes, so n - 2 and n - 1 below are indexes too.
    2. Return nums[0] when n == 1. A single house has no neighbour. The two ranges below each leave one house out, which would leave nothing.
    3. Leave out the last house. Self::rob_range(&nums, 0, n - 2) solves the street of houses 0 to n - 2. Assume the helper exists for now; writing it afterwards keeps rob short.
    4. Leave out the first house with Self::rob_range(&nums, 1, n - 1).
    5. Return the larger result with without_last.max(without_first), with no semicolon.
    6. Write rob_range with a slice &[i32] and the first and last index of the street. It returns the best total for the houses from start to end.
    7. Create prev as let mut, set to 0: the best total up to two houses before the current one.
    8. Create curr the same way: the best total up to the house just before the current one. Both start at 0 because nothing has been robbed before the first house of the range.
    9. Loop i over start..=end. The ..= includes end; .. would stop one house early.
    10. Compute best as curr.max(prev + nums[i]): skip house i, or rob it and add its money to the total from two houses back.
    11. Move both totals one house to the right: prev = curr; first, then curr = best;. In the other order, prev would receive the new value instead of the old one.
    12. Return curr after the loop.

    The solution in Rust

    impl Solution {
        fn rob(nums: Vec<i32>) -> i32 {
            let n = nums.len();
            if n == 1 {
                return nums[0];
            }
            let without_last = Self::rob_range(&nums, 0, n - 2);
            let without_first = Self::rob_range(&nums, 1, n - 1);
            without_last.max(without_first)
        }
    
        fn rob_range(nums: &[i32], start: usize, end: usize) -> i32 {
            let mut prev = 0;
            let mut curr = 0;
            for i in start..=end {
                let best = curr.max(prev + nums[i]);
                prev = curr;
                curr = best;
            }
            curr
        }
    }
    

    For [1, 2, 3, 1], the helper runs twice. The values of prev and curr after each house are:

    houses 0..=2: i=0 prev=0 curr=1; i=1 prev=1 curr=2; i=2 prev=2 curr=4
    houses 1..=3: i=1 prev=0 curr=2; i=2 prev=2 curr=3; i=3 prev=3 curr=3
    

    The first street gives 4, the second 3, and rob returns 4.

    Rust details worth a closer look:

    • Slices. rob_range takes &[i32], a borrowed view of a run of numbers. &nums, a reference to a Vec<i32>, coerces to it, and rob can lend the vector twice because the helper only reads it.
    • Type inference. prev and curr start as the literal 0. Rust infers i32 from prev + nums[i] and from the return type.
    • max is a method. curr.max(prev + nums[i]) and without_last.max(without_first) return the larger of two integers.
    • usize subtraction. n - 2 is only safe because n == 1 returned earlier, and the lesson's constraint says n is at least 1. A usize can't go below zero: in a debug build 1usize - 2 panics, and in an optimized build it wraps to a huge number.
    • Last expressions. without_last.max(without_first) and curr have no semicolon, so they are what the functions return.

    Complexity

    Time: O(n). Each call of rob_range visits at most n - 1 houses, and there are two calls.

    Space: O(1). Only prev, curr and best are kept, instead of a whole table.

    Edge cases and common mistakes

    • One house. [5] returns 5 through the early return. Without it, n - 2 would be a usize underflow.
    • Two houses. They are neighbours, so you take the larger one: the ranges are 0..=0 and 1..=1. [5, 9] gives 9.
    • Treating the circle as a line. On [400, 0, 0, 400, 0, 0, 400], a street solution gives 1200, but the circle allows 800.
    • Only checking one range. For [1, 3, 1, 3, 100] the street without the last house gives 6 and the street without the first gives 103. For the mirrored [100, 3, 1, 3, 1] the results swap. Both ranges are needed, because the best plan may leave out either end.
    • Zeros. Houses with no money are fine: [0, 0] gives 0.
    • Swapping the updates. Setting curr = best before prev = curr loses the old curr.
    • Stopping a house early. Writing start..end instead of start..=end skips the last house of each range.

    Testing

    • [2, 3, 2] gives 3.
    • [1, 2, 3, 1] gives 4.
    • [400, 0, 0, 400, 0, 0, 400] gives 800.
    • [5] gives 5.
    • [5, 9] gives 9.
    • [0, 0] gives 0.
    • [1, 2, 3] gives 3.
    • [200, 3, 140, 20, 10] gives 340.
    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free