Problem statement

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.

Link to problem

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.

Solutions

Approach 1: Dynamic Programming

This is it, this is the problem that covers everything you need to know about Dynamic Programming.

  1. Determine the choices required
    In this case, the robber has two choices, he can either rob a house he’s at or skip it.
    Now looking at the cases individually:
    Case 1: Robbing the current house
    If the current house is to be robbed, then neither the previous nor the next house can be robbed.
    Case 2: Skipping the current house
    If the current house is to be skipped, then either the previous house or the next house can be robbed.

In order to simplify, let us stick to just one direction(i.e., the robber only goes forwards).

  1. Determining which choice to pick The choice which yields the highest money must be picked, which is essentially two cases:
    Case 1: Robbing the current house: Loot of that house and the loot of next houses Case 2: Skipping the current house: Loot of the next house and the loot of next houses.

But before we determine which choice to pick, let us look at how one should approach a Dynamic Programming problem Taken from this LeetCode Discussion


Find the recurrence relation, basically a subproblem

if current_house is to be robbed, then current_house - 2 can be safely robbed along with the loot from previous houses
if current house is to be skipped, then current_house - 1 can be safely robbed along with the loot from the following houses

i.e., rob(i) = max(rob(i - 2) + current_house_value, rob(i-1))

Do note that in this approach, you start from the last house rather than the first.

Implementation in Python

def rob(self, nums: List[int]) -> int:
    return back_tracker(nums, len(nums)-1)
    
def back_tracker(nums, i):
    if i < 0:
        return 0
    return max(back_tracker(nums, i - 2) + nums[i], back_tracker(nums, i - 1))

Time Complexity: Unsure but $O(2^n)$ would be my guess
Runtime on LeetCode: Time Limit Exceeded

Optimizing the first approach

The drawback that previous approach has is that it re-computes certain values, we could save a lot of time if we store the computed values somewhere and use them instead.
Simple memoization will get us very far in this case.

Implementation in Python

def rob(self, nums: List[int]) -> int:
    memo = [-1] * (len(nums) +1 )
    return back_tracker(nums, len(nums)-1, memo)
    
def back_tracker(nums, i, memo):
    if i < 0:
        return 0
    
    if memo[i] >= 0:
        return memo[i]
    
    result = max(back_tracker(nums, i - 2, memo) + nums[i], back_tracker(nums, i - 1, memo))
    memo[i] = result
    return result

Time Complexity: $O(n)$?
Runtime on LeetCode: $36$ms

Iterative (bottom-up)

So far we’ve been starting at the last house and ending at first with maximum profits, but that is taking a toll on our robbers because they need to find out which house is the last!
One way we could help our robbers is by giving them a starting location.

Basically just reversing the logic and converting the recursive function into an iterative one.

Implementation in Python

def rob(self, nums: List[int]) -> int:
    memo = [0] * (len(nums) + 1)
    memo[1] = nums[0] # current house
    for i in range(1, len(nums)):
        current_house = nums[i]
        next_house = max(memo[i], current_house + memo[i-1])
        memo[i+1] = next_house
    return memo[len(nums)]

Slight improvement

We didn’t really need an entire list for memoization because we just used memo[i] and memo[i+1], so that can be boiled down to just two variables

Implementation in Python

def rob(self, nums: List[int]) -> int:
    prev_prev_house = 0
    prev_house = 0
            
    for num in nums:
        current_house = max(prev_prev_house + num, prev_house)
        prev_prev_house = prev_house
        prev_house = current_house
    
    return prev_house

Implementation in Rust

fn rob(nums: Vec<i32>) -> i32 {
    let mut prev_prev_house = 0;
    let mut prev_house = 0;

    for num in nums.into_iter() {
        let current_house = std::cmp::max(prev_prev_house + num, prev_house);
        prev_prev_house = prev_house;
        prev_house = current_house;
    }
    return prev_house
}

Far better and far easier to digest, whew, that was a long one!
Time Complexity: $O(n)$
Runtime on LeetCode: $2$ms

That was a long one. But I feel quite confident with Dynamic Programming now so that’s a very big plus.


House Robber II - Problem Statement

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. All houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, adjacent houses have a security system 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.

Link to problem

Example 1

Input: nums = [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.

Example 2

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.

Solution

This was pretty much the same problem as above but by skipping the first and last house because they’re next to each other. Which is why I did not create another post for this.

Implementation in Rust

fn rob(nums: Vec<i32>) -> i32 {
    let skip_first = &nums[1..];
    let skip_last = &nums[0..nums.len() - 1];

    let rob_skip_first = helper(skip_first.to_vec());
    let rob_skip_last = helper(skip_last.to_vec());

    let max = std::cmp::max(rob_skip_first, rob_skip_last);

    if nums[0] > max {
        return nums[0];
    }

    max
}

fn helper(nums: Vec<i32>) -> i32 {
    let mut prev_prev_house = 0;
    let mut prev_house = 0;

    for num in nums.into_iter() {
        let current_house = std::cmp::max(prev_prev_house + num, prev_house);
        prev_prev_house = prev_house;
        prev_house = current_house;
    }
    return prev_house
}

Time Complexity: $O(n)$, it’s the same problem but runs two times.
Runtime on Leetcode: $1$ms