198. House Robber
Problem Description
In this problem, you take on the role of a professional burglar who is aiming to rob houses lined up along a street. Every house has some money, but there's a twist: the houses are equipped with connected security systems. If you rob two adjacent houses on the same night, the security system will detect the break-ins and alert the police. Your goal is to figure out the maximum amount of money you can steal tonight without triggering any alarms.
So, you're presented with an array of integers nums
, where nums[i]
represents the amount of money stashed in the i
-th house. The challenge is to find the optimal combination of houses to rob that will maximize your total loot, keeping in mind that you cannot rob adjacent houses.
It's a classic optimization problem that requires you to look for the best strategy to maximize your gains without breaking the constraint (the alarm system in this case).
Intuition
To get to the heart of this problem, let's discuss the intuition behind the solution.
The main idea here is dynamic programming, which involves breaking the problem down into smaller subproblems and building up a solution from the answers to those subproblems.
Now, consider the following choices at each house: either you rob it or you don't. If you decide to rob house i
, you cannot rob house i-1
, but you are free to rob house i-2
and before. If you decide not to rob house i
, your best robbery amount up to house i
is the same as if you were standing at house i-1
.
Let's define two variables: f
and g
. We'll use f
to track the maximum amount we can rob up to the current house if we don't rob this house, and g
to track the maximum amount if we do rob it.
We update f
like this: We take the max between the previous f
(not robbing the previous house) and g
(robbing the previous house) because for the current house, we are not robbing it, so we are free to choose the maximum loot collected from the previous two states.
On the other hand, we update g
by adding the current house's money to the previous f
, because if we decide to rob this house, we can't rob the previous one, so we add to f
(the max money robbed without including the previous house).
Lastly, we have to return the maximum amount we can rob including or excluding the last house, which means we return the max between f
and g
.
The elegance of this solution lies in its simplicity and the fact that it takes advantage of overlapping subproblems, typical in dynamic programming solutions, and calculates the result in a bottom-up manner.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses a dynamic programming approach, which is a method for solving complex problems by breaking them down into simpler subproblems. It computes solutions to these subproblems just once and stores their results—typically in an array—to avoid the need for redundant calculations. However, in this particular problem, we don't even need an array; we only use two variables to store the results since we move through the nums
array sequentially and only require information from the previous step.
Here's a step-by-step explanation of the code that was provided:
-
Initialize two variables,
f
andg
.f
represents the maximum amount of money that can be robbed up to the current house without robbing it, andg
represents the maximum amount with robbing it.f = g = 0
-
Iterate over each amount
x
in thenums
array representing the money at each house.for x in nums:
-
Update the values of
f
andg
for each house:f
is updated to the maximum of the previousf
org
. This makesf
the maximum amount of money we could have if we chose not to rob this house (i
). We can use the maximum from the previous state since robbing the current house is out of the question.
f = max(f, g)
g
is updated by adding the current house's stashx
to the previous non-robbed maximumf
. This is because if we rob the current house, we must skip the previous one, hence we add to the previousf
.
g = f + x
Within the for loop, this updates
f
andg
at each house while maintaining the condition that we never rob two adjacent houses. -
After the loop ends, the maximum amount of money that can be robbed will be the maximum of
f
andg
.return max(f, g)
It's important to note that f
and g
are being used to keep the space complexity of the solution at O(1), meaning it doesn't require any extra space proportional to the input size. This algorithm overall has O(n) time complexity since it requires one pass through the nums
array, where n
is the number of elements in the array.
In conclusion, the implementation keeps track of two possibilities at each house (to rob or not to rob) and makes decisions that ensure the highest amount of money robbed without ever triggering the alarm system.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
To illustrate the solution approach, let's use a small example where the nums
array representing money at each house is [2, 7, 9, 3, 1]
.
Here's the step-by-step walkthrough:
-
Initialize
f
andg
to0
. These variables will hold the maximum money that can be robbed so far without and with robbing the current house, respectively. -
Start iterating over the array
nums
.- For the first house (
nums[0] = 2
), we can only rob it since there's no previous house. Therefore,f
remains0
as we are not robbing this house, andg
is updated tof + nums[0]
, which is also2
.
- For the first house (
-
Move to the second house (
nums[1] = 7
):- Update
f
tomax(f, g)
, which was from the previous iteration. Heref
becomesmax(0, 2)
sof
is now2
. - Update
g
tof + nums[1]
, which is2 + 7
, sog
becomes9
.
- Update
-
At the third house (
nums[2] = 9
):- Update
f
tomax(2, 9)
, sof
is now9
. - Update
g
tof (previous f before updating which is 2) + nums[2]
, which is2 + 9
, sog
is11
.
- Update
-
At the fourth house (
nums[3] = 3
):- Update
f
tomax(9, 11)
, sof
is now11
. - Update
g
tof (previous f before updating which is 9) + nums[3]
, which is9 + 3
, sog
is12
.
- Update
-
Finally, at the fifth house (
nums[4] = 1
):- Update
f
tomax(11, 12)
, sof
is now12
. - Update
g
tof (previous f before updating which is 11) + nums[4]
, which is11 + 1
, sog
becomes12
.
- Update
-
The loop has finished, and we now take the maximum of
f
andg
. Bothf
andg
are12
, so the maximum money that can be robbed is12
.
If we map out the houses we chose to rob based on g
's updates, we robbed the second and fourth houses - a total of 7 + 3 = 10
, and not the third and fifth as the walk through suggests. Let's correct the steps for the fourth and fifth houses.
For the fourth house, since we can't rob it (because we robbed the third house):
f
remains11
since we're not adding the value of the current house.g
doesn't change since we can't rob this house directly after the third house.
For the fifth house:
f
remains11
, andg
is updated with11 + 1
(the previousf
plus the current house's value), resulting ing
being12
.
In this case, we robbed the third and fifth houses (2nd house was not actually robbed), getting a total of 9 + 1 = 10
. Hence, the final maximum amount we could rob is 12
, updating our final g
with the amount from the fifth house and maintaining the rule of not robbing two adjacent houses.
Solution Implementation
1from typing import List
2
3class Solution:
4 def rob(self, nums: List[int]) -> int:
5 # Initialize two variables to store the maximum amount of money that can be
6 # robbed without adjacent thefts:
7 # prev_no_rob denotes the max amount considering the previous house wasn't robbed
8 # prev_robbed denotes the max amount considering the previous house was robbed
9 prev_no_rob, prev_robbed = 0, 0
10
11 # Iterate over all the houses
12 for current_house_value in nums:
13 # Calculate the new value for prev_no_rob by choosing the max between
14 # prev_no_rob and prev_robbed. This is because if the current house is not
15 # being robbed, then the maximum amount is the best of either not robbing
16 # the previous house or robbing it.
17 # The value for prev_robbed is updated to the sum of prev_no_rob and
18 # the value of the current house, as we can only rob the current house if
19 # the previous one was not robbed.
20 temp = max(prev_no_rob, prev_robbed)
21 prev_robbed = prev_no_rob + current_house_value
22 prev_no_rob = temp
23
24 # Return the max amount that can be robbed - either the amount if the last house
25 # was not robbed or if it was.
26 return max(prev_no_rob, prev_robbed)
27
1class Solution {
2 public int rob(int[] nums) {
3 // f represents the max profit we can get from the previous house
4 int prevNoRob = 0;
5 // g represents the max profit we can get if we rob the current house
6 int prevRob = 0;
7
8 // Iterate over all the houses in the array
9 for (int currentHouseValue : nums) {
10 // Store max profit of robbing/not robbing the previous house
11 int tempPrevNoRob = Math.max(prevNoRob, prevRob);
12
13 // If we rob the current house, we cannot rob the previous one
14 // hence our current profit is previous house's no-rob profit + current house value
15 prevRob = prevNoRob + currentHouseValue;
16
17 // Update the previous no-rob profit to be the best of robbing or not robbing the last house
18 prevNoRob = tempPrevNoRob;
19 }
20
21 // Return the max profit we can get from the last house,
22 // regardless of whether we rob it or not
23 return Math.max(prevNoRob, prevRob);
24 }
25}
26
1#include <vector>
2#include <algorithm> // For std::max
3
4class Solution {
5public:
6 // Function to calculate the maximum amount of money that can be robbed
7 int rob(vector<int>& nums) {
8 // Initialize previous and beforePrevious to store the max rob amounts
9 // at two adjacent houses we may potentially skip or rob
10 int previous = 0, beforePrevious = 0;
11
12 // Loop through each house represented in the nums vector
13 for (int num : nums) {
14 // Temporarily hold the maximum amount robbed so far
15 int temp = max(previous, beforePrevious);
16
17 // Update beforePrevious to the new amount robbed including the current house
18 beforePrevious = previous + num;
19
20 // Update previous to the amount stored in temp
21 previous = temp;
22 }
23
24 // Return the maximum amount that can be robbed without alerting the police
25 // which is the maximum of previous and beforePrevious after considering all houses
26 return max(previous, beforePrevious);
27 }
28};
29
1// Function to determine the maximum amount of money that can be robbed
2// without robbing two adjacent houses.
3function rob(nums: number[]): number {
4 // Initialize two variables:
5 // robPrevious - the maximum amount robbed up to the previous house
6 // robCurrent - the maximum amount robbed up to the current house
7 let [robPrevious, robCurrent] = [0, 0];
8
9 for (const currentHouseValue of nums) {
10 // Compute the new maximum excluding and including the current house:
11 // The new robPrevious becomes the maximum of either
12 // robPrevious or robCurrent from the previous iteration.
13 // robCurrent is updated with the sum of robPrevious (excludes the previous house)
14 // and the value of the current house, representing the choice to rob this house.
15 [robPrevious, robCurrent] = [Math.max(robPrevious, robCurrent), robPrevious + currentHouseValue];
16 }
17
18 // Return the maximum amount robbed between the last two houses considered
19 return Math.max(robPrevious, robCurrent);
20}
21
Time and Space Complexity
Time Complexity
The given code consists of a single for-loop that iterates through every element in the input list nums
. Inside this loop, the calculations involve simple arithmetic operations and a single max
function call which both have a constant time complexity (O(1)
). Since these operations are performed once for each element in the list, the time complexity is directly proportional to the length of the list. Thus, the time complexity of the code is O(n)
, where n
is the number of elements in nums
.
Space Complexity
The algorithm uses two variables (f
and g
) to keep track of the calculations at each step. No additional data structures that grow with the input size are used. This means that the space complexity is not dependent on the size of the input and is, therefore, constant. Hence, the space complexity of the code is O(1)
.
Learn more about how to find time and space complexity quickly using problem constraints.
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!