Facebook Pixel

213. House Robber II

Problem Description

You need to plan a robbery along a street where houses are arranged in a circle. This means the first house and the last house are neighbors of each other.

Each house contains a certain amount of money, represented by an integer array nums where nums[i] is the amount of money in house i.

The constraint is that houses have a security system - if you rob two adjacent houses on the same night, the police will be alerted automatically.

Your goal is to find the maximum amount of money you can rob without triggering the security system (without robbing any two adjacent houses).

The key difference from a standard house robbing problem is the circular arrangement. Since the houses form a circle:

  • If you rob the first house, you cannot rob the last house (they are adjacent)
  • If you rob the last house, you cannot rob the first house

Given the array nums, return the maximum total amount you can rob.

For example:

  • If nums = [2, 3, 2], you cannot rob house 0 and house 2 together since they are adjacent in the circle. The maximum would be 3 (robbing only house 1).
  • If nums = [1, 2, 3, 1], you could rob houses 0 and 2 for a total of 4, or just house 2 for 3, so the maximum is 4.

The solution cleverly handles this by considering two scenarios:

  1. Rob houses from index 1 to n-1 (excluding the first house)
  2. Rob houses from index 0 to n-2 (excluding the last house)

Then take the maximum of these two scenarios, effectively ensuring the first and last houses are never both robbed.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The circular arrangement creates a unique constraint: we cannot rob both the first and last houses since they're neighbors. This observation is the key to solving the problem.

Think about it this way - in a circular arrangement, we have three possible scenarios:

  1. We rob the first house but not the last house
  2. We rob the last house but not the first house
  3. We rob neither the first nor the last house

Notice that scenario 3 is automatically covered by scenarios 1 and 2 (since we're looking for the maximum, if neither first nor last gives us the best result, that case will naturally emerge).

This insight allows us to break down the circular problem into two simpler linear problems:

  • Case 1: Consider houses from index 0 to n-2 (exclude the last house)
  • Case 2: Consider houses from index 1 to n-1 (exclude the first house)

By solving these two linear subproblems separately and taking the maximum, we ensure that we never rob both the first and last houses together.

For each linear subproblem, we can use dynamic programming. At each house, we have two choices:

  • Rob the current house: We get the money from this house plus the maximum money we could have robbed up to the house before the previous one
  • Skip the current house: We keep the maximum money we could have robbed up to the previous house

The variables f and g in the solution track these two states:

  • f: Maximum money robbed without robbing the current house
  • g: Maximum money robbed if we rob the current house

The update rule f, g = max(f, g), f + x elegantly captures this logic, where x is the current house's money.

Solution Approach

The solution uses Dynamic Programming to solve this circular house robbing problem by reducing it to two linear subproblems.

Main Strategy: Since we cannot rob both the first and last houses (they're adjacent in the circle), we solve two separate linear problems:

  1. Rob from houses [1, n-1] (excluding first house)
  2. Rob from houses [0, n-2] (excluding last house)

Implementation Details:

The helper function _rob(nums) solves the linear house robbing problem using space-optimized dynamic programming:

def _rob(nums):
    f = g = 0
    for x in nums:
        f, g = max(f, g), f + x
    return max(f, g)

Here's how the state variables work:

  • f: Maximum money robbed without robbing the current house
  • g: Maximum money robbed with robbing the current house
  • x: Money in the current house

State Transition:

  • New f = max(f, g): The maximum we can get without robbing current house is the better of our previous two states
  • New g = f + x: If we rob the current house, we add its value to the maximum we had without robbing the previous house

Edge Case Handling:

if len(nums) == 1:
    return nums[0]

When there's only one house, we simply rob it since there are no adjacency constraints.

Final Solution:

return max(_rob(nums[1:]), _rob(nums[:-1]))
  • _rob(nums[1:]): Maximum money when excluding the first house
  • _rob(nums[:-1]): Maximum money when excluding the last house
  • Return the maximum of these two scenarios

Time Complexity: O(n) - We traverse the array twice Space Complexity: O(1) - Only using constant extra space (the slicing creates new arrays but the DP itself uses constant space)

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with nums = [2, 7, 9, 3, 1].

Since houses form a circle, house 0 (value=2) and house 4 (value=1) are neighbors. We need to find the maximum money without robbing adjacent houses.

Step 1: Break into two linear problems

  • Case 1: Exclude first house → Consider [7, 9, 3, 1] (indices 1-4)
  • Case 2: Exclude last house → Consider [2, 7, 9, 3] (indices 0-3)

Step 2: Solve Case 1 - [7, 9, 3, 1]

Using the _rob function with f (skip current) and g (rob current):

HouseValuef (skip)g (rob)Explanation
Initial-00Starting state
07max(0,0)=00+7=7Can rob first house for 7
19max(0,7)=70+9=9Skip house 1 or rob it for 9
23max(7,9)=97+3=10Best is robbing houses 0&2 for 10
31max(9,10)=109+1=10Adding house 3 doesn't improve

Result: max(10, 10) = 10 (rob houses at indices 1 and 3 → values 7 and 3)

Step 3: Solve Case 2 - [2, 7, 9, 3]

HouseValuef (skip)g (rob)Explanation
Initial-00Starting state
02max(0,0)=00+2=2Can rob first house for 2
17max(0,2)=20+7=7Rob house 1 for 7 is better
29max(2,7)=72+9=11Rob houses 0&2 for total 11
33max(7,11)=117+3=10Best remains 11

Result: max(11, 10) = 11 (rob houses at indices 0 and 2 → values 2 and 9)

Step 4: Final answer Return max(10, 11) = 11

The optimal solution is to rob houses at indices 0 and 2 (values 2 and 9) for a total of 11, which avoids robbing adjacent houses in the circle.

Solution Implementation

1class Solution:
2    def rob(self, nums: List[int]) -> int:
3        """
4        Rob houses arranged in a circle where you cannot rob adjacent houses.
5        Since first and last houses are adjacent, we consider two scenarios:
6        1. Rob houses from index 1 to n-1 (exclude first house)
7        2. Rob houses from index 0 to n-2 (exclude last house)
8        Return the maximum of these two scenarios.
9        """
10      
11        def rob_linear(houses: List[int]) -> int:
12            """
13            Helper function to rob houses in a linear arrangement.
14            Uses dynamic programming with two variables to track:
15            - rob_current: max money if we rob the current house
16            - skip_current: max money if we skip the current house
17            """
18            skip_current = 0  # Max money when skipping current house
19            rob_current = 0   # Max money when robbing current house
20          
21            for house_value in houses:
22                # Calculate new states
23                # If we skip this house, we take max of previous skip/rob
24                # If we rob this house, we must have skipped the previous one
25                skip_current, rob_current = max(skip_current, rob_current), skip_current + house_value
26          
27            # Return the maximum money we can rob
28            return max(skip_current, rob_current)
29      
30        # Edge case: single house
31        if len(nums) == 1:
32            return nums[0]
33      
34        # Compare two scenarios: exclude first house vs exclude last house
35        return max(rob_linear(nums[1:]), rob_linear(nums[:-1]))
36
1class Solution {
2    /**
3     * Main method to solve the house robber problem with houses arranged in a circle.
4     * Since the first and last houses are adjacent (circular arrangement),
5     * we cannot rob both of them. So we consider two scenarios:
6     * 1. Rob houses from index 0 to n-2 (exclude last house)
7     * 2. Rob houses from index 1 to n-1 (exclude first house)
8     * Return the maximum of these two scenarios.
9     * 
10     * @param nums Array representing money in each house
11     * @return Maximum amount that can be robbed
12     */
13    public int rob(int[] nums) {
14        int n = nums.length;
15      
16        // Edge case: only one house
17        if (n == 1) {
18            return nums[0];
19        }
20      
21        // Compare two scenarios: rob houses [0, n-2] vs [1, n-1]
22        return Math.max(
23            rob(nums, 0, n - 2),  // Scenario 1: include first house, exclude last
24            rob(nums, 1, n - 1)   // Scenario 2: exclude first house, include last
25        );
26    }
27
28    /**
29     * Helper method to find maximum robbery amount for a linear range of houses.
30     * Uses dynamic programming with space optimization.
31     * 
32     * @param nums Array representing money in each house
33     * @param start Starting index (inclusive)
34     * @param end Ending index (inclusive)
35     * @return Maximum amount that can be robbed in the given range
36     */
37    private int rob(int[] nums, int start, int end) {
38        // prevNotRobbed: max money when previous house is not robbed
39        int prevNotRobbed = 0;
40      
41        // prevRobbed: max money when previous house is robbed
42        int prevRobbed = 0;
43      
44        // Iterate through each house in the range
45        for (int i = start; i <= end; i++) {
46            // Calculate new max when current house is not robbed
47            // (can come from either robbing or not robbing the previous house)
48            int currentNotRobbed = Math.max(prevNotRobbed, prevRobbed);
49          
50            // Calculate new max when current house is robbed
51            // (must not rob the previous house, so use prevNotRobbed)
52            int currentRobbed = prevNotRobbed + nums[i];
53          
54            // Update states for next iteration
55            prevNotRobbed = currentNotRobbed;
56            prevRobbed = currentRobbed;
57        }
58      
59        // Return the maximum of robbing or not robbing the last house
60        return Math.max(prevNotRobbed, prevRobbed);
61    }
62}
63
1class Solution {
2public:
3    int rob(vector<int>& nums) {
4        int n = nums.size();
5      
6        // Base case: only one house
7        if (n == 1) {
8            return nums[0];
9        }
10      
11        // For circular arrangement, we have two scenarios:
12        // 1. Rob houses from index 0 to n-2 (exclude last house)
13        // 2. Rob houses from index 1 to n-1 (exclude first house)
14        // Return the maximum of both scenarios
15        return max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
16    }
17
18private:
19    int robRange(vector<int>& nums, int start, int end) {
20        // Dynamic programming approach using two variables
21        // prevMax: maximum money robbed up to the previous house
22        // currMax: maximum money robbed up to the current house
23        int prevMax = 0;
24        int currMax = 0;
25      
26        // Iterate through the specified range of houses
27        for (int i = start; i <= end; ++i) {
28            // Calculate new maximum for current position
29            // Either skip current house (take prevMax) or rob current house (prevMax + nums[i])
30            int tempMax = max(prevMax, currMax);
31            currMax = prevMax + nums[i];
32            prevMax = tempMax;
33        }
34      
35        // Return the maximum money that can be robbed
36        return max(prevMax, currMax);
37    }
38};
39
1/**
2 * House Robber II - Rob houses arranged in a circle
3 * @param nums - Array representing money in each house
4 * @returns Maximum amount of money that can be robbed
5 */
6function rob(nums: number[]): number {
7    const houseCount: number = nums.length;
8  
9    // Edge case: only one house to rob
10    if (houseCount === 1) {
11        return nums[0];
12    }
13  
14    /**
15     * Helper function to rob houses in a linear range
16     * Uses dynamic programming with space optimization
17     * @param startIndex - Starting index of the range (inclusive)
18     * @param endIndex - Ending index of the range (inclusive)
19     * @returns Maximum money that can be robbed in the given range
20     */
21    const robLinearRange = (startIndex: number, endIndex: number): number => {
22        // previousMax: max money when not robbing current house
23        // currentMax: max money when robbing current house
24        let previousMax: number = 0;
25        let currentMax: number = 0;
26      
27        // Iterate through houses in the specified range
28        for (let i: number = startIndex; i <= endIndex; i++) {
29            // Store previous values before updating
30            const tempPrevious: number = previousMax;
31            const tempCurrent: number = currentMax;
32          
33            // Update states:
34            // New previousMax = max of old previousMax and currentMax
35            // New currentMax = old previousMax + current house value
36            previousMax = Math.max(tempPrevious, tempCurrent);
37            currentMax = tempPrevious + nums[i];
38        }
39      
40        // Return the maximum of both states
41        return Math.max(previousMax, currentMax);
42    };
43  
44    // Since houses are in a circle, we cannot rob both first and last house
45    // Solution: Take maximum of two scenarios:
46    // 1. Rob houses from index 0 to n-2 (exclude last house)
47    // 2. Rob houses from index 1 to n-1 (exclude first house)
48    const robExcludingLast: number = robLinearRange(0, houseCount - 2);
49    const robExcludingFirst: number = robLinearRange(1, houseCount - 1);
50  
51    return Math.max(robExcludingLast, robExcludingFirst);
52}
53

Time and Space Complexity

The time complexity is O(n), where n is the length of the array. The algorithm calls the helper function _rob twice - once with nums[1:] and once with nums[:-1]. Each call to _rob iterates through approximately n-1 elements, performing constant time operations (max and addition) for each element. Therefore, the total time complexity is O(n-1) + O(n-1) = O(n).

The space complexity is O(n). While the helper function _rob uses only O(1) space with variables f and g, the slicing operations nums[1:] and nums[:-1] each create new arrays of size n-1, requiring O(n) additional space. The reference answer states O(1) space complexity, which would be achievable if the code used index-based iteration instead of array slicing.

Common Pitfalls

1. Not Handling Edge Cases Properly

A frequent mistake is forgetting to handle special cases, particularly when the array has only 1 or 2 elements. The circular constraint doesn't apply meaningfully to these cases.

Incorrect approach:

def rob(self, nums: List[int]) -> int:
    # Directly applying the two-scenario logic without edge case handling
    return max(self._rob(nums[1:]), self._rob(nums[:-1]))
    # This fails when nums has only 1 element because nums[1:] would be empty

Correct approach:

def rob(self, nums: List[int]) -> int:
    if len(nums) == 1:
        return nums[0]
    if len(nums) == 2:
        return max(nums[0], nums[1])
    return max(self._rob(nums[1:]), self._rob(nums[:-1]))

2. Confusing State Variables in Dynamic Programming

Many people mix up what each DP variable represents, leading to incorrect state transitions.

Incorrect approach:

def _rob(nums):
    f = g = 0
    for x in nums:
        # Wrong: Using g in both transitions
        f, g = g, g + x  # This doesn't maintain the "skip previous house" constraint
    return max(f, g)

Correct approach:

def _rob(nums):
    f = g = 0  # f: max without robbing current, g: max with robbing current
    for x in nums:
        # f becomes max of previous states, g uses old f (skip previous) + current
        f, g = max(f, g), f + x
    return max(f, g)

3. Creating Unnecessary Space Complexity

Using a full DP array when only the last two states are needed.

Space-inefficient approach:

def _rob(nums):
    n = len(nums)
    if n == 0:
        return 0
    dp = [0] * (n + 1)
    dp[1] = nums[0]
    for i in range(2, n + 1):
        dp[i] = max(dp[i-1], dp[i-2] + nums[i-1])
    return dp[n]

Space-efficient approach:

def _rob(nums):
    prev2 = prev1 = 0
    for x in nums:
        prev2, prev1 = prev1, max(prev1, prev2 + x)
    return prev1

4. Misunderstanding the Circular Constraint

Some attempt to handle the circular nature within the DP logic itself, making the solution unnecessarily complex.

Overly complex approach:

def rob(self, nums: List[int]) -> int:
    # Trying to track whether first house was robbed throughout the DP
    # This leads to complicated state management and is error-prone
    dp = [[0, 0] for _ in range(len(nums))]  # [robbed_first, not_robbed_first]
    # Complex logic follows...

Simple and correct approach:

def rob(self, nums: List[int]) -> int:
    # Simply solve two separate linear problems
    if len(nums) == 1:
        return nums[0]
    return max(rob_linear(nums[1:]), rob_linear(nums[:-1]))

5. Array Slicing Performance Concerns

While nums[1:] and nums[:-1] are clean, they create new arrays. In performance-critical scenarios, using indices might be better.

More memory-efficient approach:

def rob(self, nums: List[int]) -> int:
    def rob_range(start, end):
        f = g = 0
        for i in range(start, end):
            f, g = max(f, g), f + nums[i]
        return max(f, g)
  
    if len(nums) == 1:
        return nums[0]
    return max(rob_range(1, len(nums)), rob_range(0, len(nums)-1))
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More