Please see github.com/anusha-murali for all of my repositories.
This is my solution for the House Robber problem from LeetCode (Problem #198).
Problem: 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.
In order to find the best plan that maximizes his loot, the robber proceeds as follows. His recursion algorithm is called rob()
, which takes an array of integers as input and returns the maximum amount of loot he can get without triggering a police alert.
He first starts at House 0, which has nums[0]
amount of money, and calls rob()
on nums[2:]
as he cannot rob the adjacent house. rob(nums[2:])
returns the maximum possible amount looted starting from House 2. So, the total he could optimally loot starting from House 0 is nums[0] + rob(nums[2:])
. He stores this in loot
and nums[0] + rob(nums[2:])
is larger than $-\infty$.
He then moves to House 1, which has [nums[1]
amount of money, and calls rob()
on nums[3:]
as he cannot rob the adjacent house. rob(nums[3:])
returns the maximum possible amount looted starting from House 3. So, the total he could optimally loot starting from House 1 is nums[1] + rob(nums[3:])
. He compares this to current loot
and updates loot
with max(loot, nums[i] + rob(nums[i+2:]))
, where i = 1
. He continues in this manner.
Hence, the code for the top-down recursion is as follows:
def rob(nums):
if len(nums) == 0:
return 0
loot = float('-inf')
for i in range(len(nums)):
loot = max(loot, nums[i] + rob(nums[i+2:]))
return loot
This problem is similar to the Rod Cutting problem and therefore the time complexity is exponential in $n$.
This approach is identical to the top-down recursive solution above, except that we will save the result of each subproblem. Each recursive call now first checks to see whether it has previously solved this subproblem. If so, it returns the saved value, which avoids repeated calculations of the subproblem; if not, it computes the value and saves it.
We save the loot amount for the robber starting from House 0 to House $n$ in array lootArray[0..n]
.
def rob(nums):
lootArray = ['-inf' for i in range(len(nums))]
return robMemoized(nums, lootArray)
def robMemoized(nums, lootArray):
if len(nums) == 0:
return 0
if lootArray[len(nums)-1] != '-inf':
return lootArray[len(nums)-1]
loot = float(0)
for i in range(len(nums)):
loot = max(loot, nums[i] + robMemoized(nums[i+2:], lootArray))
lootArray[len(nums)-1] = loot
return loot
Runtime: Unlike Solution 1, the memoized version solves each subproblem only once. We note that the for loop results in an arithmetic series, which therefore results in a runtime of $\Theta(n^2)$.
In the bottom-up approach, we solve the sub-problems of sizes $i = 0, 1, \ldots, n$ in increasing order. Hence, when a problem of size $k$ is encountered, it can make use of all the sub-problems of size $i < k$, as they have been already been computed. The results of the subproblems are saved in the revenue array $r$, where we initialize $r[0] = 0$ as there is no revenue from a rod of lengh 0.
def rob(self, nums: List[int]) -> int:
if len(nums) <= 2:
return max(nums)
lootArray = []
lootArray.append(nums[0])
lootArray.append(max(nums[0], nums[1]))
lootArray = lootArray + [0 for i in range(2,len(nums))]
for i in range(2,len(nums)):
loot = max(nums[0], nums[1])
for j in range(2, i+1):
loot = max(loot, nums[j] + lootArray[j-2])
lootArray[i] = loot
return lootArray[len(nums)-1]
Runtime: Due to the doubly-nested loop structure, the bottom-up rob()
has a runtime of $\Theta(n^2)$.
We maintain two variables, steal
and skip
, where steal
corresponds to the larger of the current sums and skip
corresponds to the smaller of the two. We iterate through each house and add the current value to skip
, compare the sum to the current value of steal
, and pick the larger of the two. During each iteration, the current steal
replaces skip
and the current maximum value is assigned to steal
. This gurantees that we always add the current house nums[i]
alternatively to either of these sums, without triggering a police alarm. At each end of an iteration, steal
contains the largest possible loot so far. The code is as follows:
def rob(nums):
if len(nums) == 1:
return nums[0]
steal = max(nums[0], nums[1])
skip = nums[0]
for i in range(2, len(nums)):
currMax = max(steal, skip + nums[i])
skip = steal
steal = currMax
return steal
Runtime: We have only one for
loop iterating through each of the $n$ houses, and therefore the runtime is $O(n)$.
In addition to finding the maximum possible amount the robber can take, we are also interested in finding the house numbers that he robs, which led to the maximum loot.
We use one additional array, $s$, which saves the house numbers that need to be initially checked. We can then backtrack from the last element in this array and select the non-consecutive house numbers, which correspond to the houses that the robber would visit.
def rob(nums):
if len(nums) == 1:
return nums[0]
steal = max(nums[0], nums[1])
skip = nums[0]
s = []
s.append(0) # Start comparing from the 1st house
s.append(1)
for i in range(2, len(nums)):
if steal < skip + nums[i]:
currMax = skip + nums[i]
if i > 0:
s.append(i) # This is a potential house to rob
skip = steal
steal = currMax
return (steal, s)
def getHouses(nums):
(maxLoot, s) = rob(nums)
houseList = []
houseList.append(s[-1]) # Start from the last house in s
for i in range(len(s) - 2, -1, -1):
if s[i] != houseList[-1] - 1:
houseList.append(s[i])
return houseList.reverse()
Back to Dynamic Programming Problems