House Robber
MedPrompt
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.
Example 1:
Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.Example 2:
Input: nums = [2,7,9,3,1]
Output: 12
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1).
Total amount you can rob = 2 + 9 + 1 = 12.
Constraints:
1 <= nums.length <= 1000 <= nums[i] <= 400
Approaches
4 approaches with complexity analysis and trade-offs.
This approach directly models the decision-making process for each house using a recursive function. For each house, we explore two possibilities: either rob it and skip the next one, or skip it and move to the next one. This leads to exploring all possible valid combinations of robbing houses.
Algorithm
- Create a recursive function, say
robFrom(index, nums). - The base case for the recursion is when
indexis out of bounds of thenumsarray (i.e.,index >= nums.length). In this case, return 0 as no more houses can be robbed. - In the recursive step, for the current house at
index, there are two choices:- Rob the current house: The profit is
nums[index]plus the result of a recursive call starting fromindex + 2(since the next house cannot be robbed). - Skip the current house: The profit is the result of a recursive call starting from
index + 1.
- Rob the current house: The profit is
- Return the maximum of these two choices.
- The initial call to the function will be
robFrom(0, nums).
Walkthrough
This method involves creating a recursive function that, for each house, calculates the maximum profit by considering two scenarios: robbing the current house or skipping it. If we rob the house at index i, we gain nums[i] and must skip the next house, so we recursively call the function for index i + 2. If we skip house i, we can move to the next house, so we recursively call for index i + 1. The function returns the maximum of these two outcomes. This process continues until we've considered all houses.
class Solution { public int rob(int[] nums) { return robFrom(0, nums); } private int robFrom(int i, int[] nums) { // Base case: If we are past the last house, we can't rob anymore. if (i >= nums.length) { return 0; } // Option 1: Rob the current house (nums[i]) and skip the next one (i+1). int robCurrent = nums[i] + robFrom(i + 2, nums); // Option 2: Skip the current house and consider the next one (i+1). int skipCurrent = robFrom(i + 1, nums); // Return the maximum of the two options. return Math.max(robCurrent, skipCurrent); }}The main drawback is its performance. The number of recursive calls grows exponentially with the number of houses, as the function repeatedly solves the same subproblems. For an input array of size n, the time complexity is O(2^n).
Complexity
Time
O(2^n)
Space
O(n)
Trade-offs
Pros
Simple to understand and implement, as it directly follows the problem's logic.
Cons
Extremely inefficient due to exponential time complexity.
Recalculates the same subproblems multiple times, leading to a 'Time Limit Exceeded' error on most platforms for larger inputs.
This approach improves upon the brute-force recursion by using memoization to avoid recomputing results for the same subproblems. We use an array (or a hash map) to store the results of subproblems once they are calculated. When the function is called again with the same input, it returns the stored result instead of re-calculating it.
Algorithm
- Create a memoization array,
memo, of the same size asnums, initialized with a value indicating that the state has not been computed (e.g., -1). - Create a recursive helper function,
robFrom(index, nums, memo). - The base case is the same: if
indexis out of bounds, return 0. - Before computing, check if
memo[index]has been calculated. If so, returnmemo[index]. - If not, calculate the result using the same recurrence as the brute-force approach:
max(nums[index] + robFrom(index + 2, ...), robFrom(index + 1, ...)). - Store this result in
memo[index]before returning it. - The initial call will be
robFrom(0, nums, memo).
Walkthrough
To optimize the brute-force approach, we can store the results of subproblems in a memoization table (an array in this case). This technique is a form of top-down dynamic programming. We create a memo array, where memo[i] will store the maximum amount of money that can be robbed from house i to the end of the street.
Before making the recursive calls to compute the result for index i, we first check if memo[i] has already been computed. If it has, we simply return the stored value. Otherwise, we perform the computation, store the result in memo[i], and then return it. This ensures that each subproblem is solved only once.
import java.util.Arrays; class Solution { private int[] memo; public int rob(int[] nums) { memo = new int[nums.length]; Arrays.fill(memo, -1); // Initialize memo table with -1 return robFrom(0, nums); } private int robFrom(int i, int[] nums) { if (i >= nums.length) { return 0; } // If we have already solved this subproblem, return the stored result. if (memo[i] != -1) { return memo[i]; } // Rob current house + max from house i+2 onwards int robCurrent = nums[i] + robFrom(i + 2, nums); // Skip current house, get max from house i+1 onwards int skipCurrent = robFrom(i + 1, nums); // Store the result in the memo table and return it. memo[i] = Math.max(robCurrent, skipCurrent); return memo[i]; }}This optimization drastically reduces the time complexity from exponential to linear, O(n), because each state robFrom(i, ...) is computed at most once. The space complexity is O(n) to store the memoization table and for the recursion call stack.
Complexity
Time
O(n)
Space
O(n)
Trade-offs
Pros
Significantly more efficient than brute-force.
Guarantees that each subproblem is solved only once.
Maintains the recursive structure which can be intuitive.
Cons
Uses O(n) extra space for the memoization table and recursion stack.
Can lead to stack overflow for very large
nif the recursion depth is too high (though not an issue with the given constraints).
This approach uses a bottom-up dynamic programming strategy. We build a DP array, where dp[i] represents the maximum amount of money that can be robbed up to house i. We iterate through the houses and calculate dp[i] based on the values for previous houses, effectively building the solution from the smallest subproblem to the final answer.
Algorithm
- Handle edge cases: if
numsis empty, return 0; if it has one element, return that element. - Create a DP array,
dp, of the same size asnums. - Initialize the base cases:
dp[0] = nums[0]dp[1] = max(nums[0], nums[1])
- Iterate from
i = 2tonums.length - 1. - In each iteration, apply the recurrence relation:
dp[i] = max(dp[i-1], dp[i-2] + nums[i]).dp[i-1]represents the choice of not robbing housei.dp[i-2] + nums[i]represents the choice of robbing housei.
- The final answer is the last element of the
dparray,dp[nums.length - 1].
Walkthrough
Instead of a top-down recursive approach, we can solve the problem iteratively in a bottom-up fashion. We define a dp array where dp[i] stores the maximum amount of money that can be robbed from the first i+1 houses (i.e., from house 0 to house i).
The state transition is based on the same decision at each house i:
- Don't rob house
i: The max profit is the same as the max profit up to housei-1, which isdp[i-1]. - Rob house
i: The max profit isnums[i]plus the max profit up to housei-2(since we can't robi-1), which isnums[i] + dp[i-2].
So, the recurrence relation is dp[i] = max(dp[i-1], dp[i-2] + nums[i]).
We handle the base cases for the first one or two houses and then iterate through the rest of the array to fill the dp table. The final answer is the value at the last index of the dp array.
class Solution { public int rob(int[] nums) { if (nums == null || nums.length == 0) { return 0; } if (nums.length == 1) { return nums[0]; } int[] dp = new int[nums.length]; // Base cases dp[0] = nums[0]; dp[1] = Math.max(nums[0], nums[1]); // Fill the rest of the dp table for (int i = 2; i < nums.length; i++) { // dp[i-1] -> Don't rob house i // nums[i] + dp[i-2] -> Rob house i dp[i] = Math.max(dp[i-1], nums[i] + dp[i-2]); } return dp[nums.length - 1]; }}This approach has a time complexity of O(n) and a space complexity of O(n) for the DP array. It avoids recursion, thus eliminating the risk of stack overflow and the overhead associated with function calls.
Complexity
Time
O(n)
Space
O(n)
Trade-offs
Pros
Efficient with linear time complexity.
Avoids recursion, so no risk of stack overflow.
Generally slightly faster than memoization due to no recursion overhead.
Cons
Uses O(n) extra space for the DP array, which can be optimized.
This is the most efficient approach. It builds upon the iterative DP solution by noticing that the calculation for the current house i only depends on the results for the previous two houses (i-1 and i-2). Therefore, we don't need to store the entire DP array. We can use just two variables to keep track of the necessary previous results, reducing the space complexity to constant.
Algorithm
- Initialize two variables,
rob1 = 0androb2 = 0.rob1will store the maximum profit ending at the previous house considered, androb2will store the maximum profit ending at the house before that. - Iterate through each amount
numin thenumsarray. - In each iteration, calculate the maximum profit if we consider the current house
num. This would bemax(num + rob2, rob1).num + rob2: Rob the current house (num) plus the max profit from two houses ago (rob2).rob1: Don't rob the current house, so the max profit is the same as the max profit from the previous house (rob1).
- Store this new maximum in a temporary variable, say
temp. - Update
rob2to be the oldrob1. - Update
rob1to betemp. - After iterating through all the houses,
rob1will hold the maximum possible profit. Returnrob1.
Walkthrough
We can optimize the space complexity of the iterative DP approach. Observing the recurrence relation dp[i] = max(dp[i-1], dp[i-2] + nums[i]), we see that to compute the maximum profit for the current house, we only need the results from the previous two houses. This means we don't need to maintain the entire dp array.
We can use two variables to track the required information. Let's use rob1 to store the maximum profit up to the previous house (dp[i-1]) and rob2 to store the maximum profit up to the house before that (dp[i-2]).
As we iterate through the houses, we calculate the current maximum profit, current_max = max(rob1, rob2 + nums[i]). Then, for the next iteration, the previous rob1 becomes the new rob2, and current_max becomes the new rob1.
class Solution { public int rob(int[] nums) { if (nums == null || nums.length == 0) { return 0; } int rob1 = 0; // Represents the max profit ending at the previous house (dp[i-1]) int rob2 = 0; // Represents the max profit ending at the house before previous (dp[i-2]) // [rob2, rob1, num, num+1, ...] for (int num : nums) { // The new max profit is either robbing the current house (num + rob2) // or not robbing it (rob1). int temp = Math.max(num + rob2, rob1); // Update pointers for the next iteration rob2 = rob1; rob1 = temp; } return rob1; }}This optimized solution maintains the O(n) time complexity but reduces the space complexity to O(1), making it the most efficient approach.
Complexity
Time
O(n)
Space
O(1)
Trade-offs
Pros
Optimal solution with linear time and constant space complexity.
Very efficient and practical for large inputs.
Cons
The logic can be slightly less intuitive to grasp initially compared to the direct DP array approach.
Solutions
Solution
function rob ( nums ) { const n = nums . length ; const f = Array ( n ). fill ( - 1 ); const dfs = i => { if ( i >= n ) { return 0 ; } if ( f [ i ] < 0 ) { f [ i ] = Math . max ( nums [ i ] + dfs ( i + 2 ), dfs ( i + 1 )); } return f [ i ]; }; return dfs ( 0 ); }# greedy class Solution : def rob ( self , nums : List [ int ]) -> int : not_rob , rob = 0 , nums [ 0 ] for num in nums [ 1 :]: # must max check # eg. first robbed 99999, then following ones are just 3,3,3,3,3 not_rob , rob = rob , max ( num + not_rob , rob ) return rob ############ class Solution ( object ): def rob ( self , nums ): """ :type nums: List[int] :rtype: int """ if len ( nums ) == 0 : return 0 if len ( nums ) <= 2 : return max ( nums ) dp = [ 0 for i in range ( 0 , 2 )] dp [ 0 ] = nums [ 0 ] dp [ 1 ] = max ( nums [ 1 ], nums [ 0 ]) for i in range ( 2 , len ( nums )): dp [ i % 2 ] = max ( dp [( i - 1 ) % 2 ], dp [( i - 2 ) % 2 ] + nums [ i ]) return dp [( len ( nums ) - 1 ) % 2 ]public class House_Robber { class Solution { public int rob ( int [] nums ) { if ( nums == null || nums . length == 0 ) { return 0 ; } // dp[i] means until i, max possible amount int [] dp = new int [ nums . length + 1 ]; dp [ 0 ] = 0 ; dp [ 1 ] = nums [ 0 ]; for ( int i = 2 ; i <= nums . length ; i ++) { // 2 cases: rob current house, not rob current dp [ i ] = Math . max ( nums [ i - 1 ] + dp [ i - 2 ], dp [ i - 1 ]); } return dp [ nums . length ]; } } } ///////// class Solution { public int rob ( int [] nums ) { int a = 0 , b = nums [ 0 ]; for ( int i = 1 ; i < nums . length ; ++ i ) { int c = Math . max ( nums [ i ] + a , b ); a = b ; b = c ; } return b ; } }// rob[i + 1] = nums[i] + skip[i] // If we rob at house[i], we must skip house[i-1] // skip[i + 1] = max(rob[i - 1], skip[i - 1]) // If we skip house[i], we can pick the maximum from robbing or skipping house[i-1] class Solution { public: int rob ( vector < int >& nums ) { int n = nums . size (); int a = 0 , b = nums [ 0 ]; for ( int i = 1 ; i < n ; ++ i ) { int c = max ( nums [ i ] + a , b ); a = b ; b = c ; } return b ; } };Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.