House Robber II

Problem

Given a list of non-negative integers representing the amount of money of each house arranged in a circle, return the maximum amount of money you can rob without robbing two adjacent houses.

  • 1 ≤ nums.length ≤ 100
  • 0 ≤ nums[i] ≤ 1000

Example

Input: nums = [2,3,2]
Output: 3

The houses are arranged in a circle: house 0 with 2, house 1 with 3, and house 2 with 2. Robbing house 0 and house 2 is not allowed because they are adjacent in the circle. The optimal choice is to rob house 1 only, yielding 3. The algorithm solves this by considering two linear cases: robbing houses from index 0 to n-2, and robbing houses from index 1 to n-1, then taking the maximum of these two results.

Approach

Straightforward Solution

A brute-force approach would try all subsets of houses excluding adjacent ones, but the circular constraint complicates this, making naive recursion or backtracking exponential and inefficient.

Core Observation

The circular adjacency constraint means the first and last houses cannot both be robbed. This breaks the straightforward linear dynamic programming approach used in the simpler House Robber problem.

Path to Optimal

The key insight is to transform the circular problem into two linear subproblems by excluding either the first or the last house. Each subproblem can be solved using the classic linear House Robber DP approach. Taking the maximum of these two solves the original circular problem optimally.

Optimal Approach

Define a helper function that computes the maximum robbery amount for a linear list of houses using DP with memoization. Then, compute the maximum for nums excluding the last house and nums excluding the first house, and return the maximum of these two results. This approach respects the circular constraint and runs efficiently.

Time

O(n)

Each linear subproblem is solved with memoized recursion that visits each house index once, resulting in O(n) time. Since two subproblems are solved, total time remains O(n).

Space

O(n)

Memoization dictionaries store results for each house index in the linear subproblems, requiring O(n) auxiliary space. The recursion stack depth is also O(n) in the worst case.

Pattern Spotlight

Dynamic Programming (Linear Subproblem Decomposition)

When a circular dependency prevents direct DP application, break the problem into multiple linear subproblems by excluding conflicting elements, solve each independently with DP, and combine results to respect the original constraints.

Solution

1class Solution:
2 def rob(self, nums: list[int]) -> int:
3 if len(nums) == 1:
4 return nums[0]
5
6 def helper(sub_nums):
7 memo = {}
8 def dp(i):
9 if i < 0:
10 return 0
11 if i in memo:
12 return memo[i]
13
14 result = max(sub_nums[i] + dp(i - 2), dp(i - 1))
15 memo[i] = result
16 return result
17 return dp(len(sub_nums) - 1)
18
19 return max(helper(nums[:-1]), helper(nums[1:]))

Step-by-Step Solution

1

Handle Single House Edge Case

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

Objective

To immediately return the amount in the single house when the input list contains only one house.

Key Insight

When there is only one house, the circular adjacency constraint is irrelevant, and the maximum amount is simply the value of that house. Handling this edge case separately avoids unnecessary recursion and simplifies the main logic.

Interview Quick-Check

State & Boundaries

Check if the input list length is 1 and return the single value immediately to handle the trivial case.

2

Define Memoized DP Helper for Linear House Robbery

6def helper(sub_nums):
7 memo = {}
8 def dp(i):
9 if i < 0:
10 return 0
11 if i in memo:
12 return memo[i]
13
14 result = max(sub_nums[i] + dp(i - 2), dp(i - 1))
15 memo[i] = result
16 return result
17 return dp(len(sub_nums) - 1)

Objective

To compute the maximum robbery amount for a linear list of houses using top-down DP with memoization.

Key Insight

The classic House Robber problem can be solved by deciding at each house whether to rob it (and add its value to the result two houses before) or skip it (and take the result from the previous house). Memoization avoids redundant computations by caching results for each index. This helper function encapsulates this logic for any linear subarray of houses.

Interview Quick-Check

Core Logic

At each house index i, the maximum amount is the max between robbing house i plus dp(i-2), or skipping house i and taking dp(i-1). Memoization ensures each state is computed once.

State & Boundaries

Base case returns 0 when index i is less than 0, representing no houses left to rob.

Common Pitfalls & Bugs

Forgetting to memoize results leads to exponential recursion. Also, incorrect base cases can cause index errors or incorrect results.

3

Compute Maximum Robbery by Comparing Two Linear Cases

19return max(helper(nums[:-1]), helper(nums[1:]))

Objective

To solve the circular problem by applying the helper to two linear subproblems and returning the maximum result.

Key Insight

Because the first and last houses are adjacent, robbing both is forbidden. By computing the maximum robbery amount excluding the last house and excluding the first house, the algorithm covers all valid scenarios. Taking the maximum of these two results yields the optimal solution for the circular arrangement.

Interview Quick-Check

Core Logic

The final answer is the maximum between robbing houses from index 0 to n-2 and from index 1 to n-1, ensuring the circular adjacency constraint is respected.

Common Pitfalls & Bugs

Failing to exclude either the first or last house in one of the subproblems can lead to invalid solutions that rob adjacent houses in the circle.

Line Analysis

This solution has 3 Critical lines interviewers watch for.

Line 14 Critical
result = max(sub_nums[i] + dp(i - 2), dp(i - 1))

Calculate max robbery by choosing to rob house i plus dp(i-2) or skip house i and take dp(i-1).

This recurrence captures the core DP decision: robbing the current house plus the best from two houses before, or skipping it to take the best from the previous house, ensuring no two adjacent houses are robbed.

Line 11 Critical
if i in memo:

Return cached result if index i has been computed before.

Memoization lookup avoids recomputation, ensuring each subproblem is solved once.

Line 19 Critical
return max(helper(nums[:-1]), helper(nums[1:]))

Return the maximum robbery amount between robbing houses excluding the last or excluding the first.

This line implements the key insight that the circular problem can be solved by comparing two linear subproblems, each excluding one of the conflicting houses, ensuring the circular adjacency constraint is respected.

Line 7 Core
memo = {}

Initialize a memoization dictionary to cache results for subproblems.

Memoization prevents redundant recursive calls, reducing exponential recursion to linear time complexity.

Line 9 Core
if i < 0:

Return 0 if index i is less than 0, representing no houses left.

This base case terminates recursion and correctly models that no money can be robbed from an empty set of houses.

Line 15 Core
memo[i] = result

Store the computed result in memo for index i.

Caching the result ensures that future calls for index i return instantly, preventing exponential recursion.

Line 17 Core
return dp(len(sub_nums) - 1)

Return the maximum robbery amount for the entire sublist.

Starts the recursion from the last house in the sublist, triggering the full DP computation.

Line 3 Core
if len(nums) == 1:

Check if the input list contains only one house.

This early check handles the trivial edge case where the circular constraint is irrelevant, allowing immediate return of the single house's value without further computation.

Line 4 Core
return nums[0]

Return the amount in the single house.

Returning the single house's value is correct because no adjacency constraints apply when only one house exists.

Line 8 Core
def dp(i):

Define the recursive dp function that computes max robbery up to index i.

This function models the problem as a state machine where each state represents the maximum robbery amount considering houses up to index i.

Line 12 Core
return memo[i]

Retrieve memoized result for index i.

Accessing cached results is essential for efficient DP.

Line 16 Core
return result

Return the computed maximum robbery amount for index i.

Returns the optimal solution for the subproblem up to house i, enabling recursive buildup of the final answer.

Line 6 Standard
def helper(sub_nums):

Define a helper function to solve the linear House Robber problem on a sublist.

Encapsulating the linear DP logic in a helper function allows reuse for both subproblems derived from the circular input.

Line 10 Standard
return 0

Return 0 for negative indices.

Ensures safe recursion termination and prevents index errors.

Test Your Understanding

Why does solving two linear subproblems—excluding the first house in one and the last house in the other—correctly handle the circular adjacency constraint?

Because the first and last houses are adjacent in the circle, they cannot both be robbed. By excluding the last house in one subproblem and the first house in the other, each subproblem represents a linear arrangement without circular adjacency, ensuring no invalid adjacent robberies. Taking the maximum of these two covers all valid possibilities.

Related Problems

Dynamic Programming pattern

Don't just read it. Drill it.

Reconstruct House Robber II from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the House Robber II drill

or drill a free problem