Using the leetcode 198 to walk through how to use the DP framework to do this problem.
1. A function or array that answers the problem for a given state
First, we need to decide on state variables. As a reminder, state variables should be fully capable of describing a scenario. Imagine if you had this scenario in real life - you’re a robber and you have a lineup of houses. If you are at one of the houses, the only variable you would need to describe your situation is an integer - the index of the house you are currently at. Therefore, the only state variable is an integer, say i
, that indicates the index of a house.
If the problem has an added constraint such as “you are only allowed to rob up to k houses”, then k would be another necessary state variable. This is because being at, say house 4 with 3 robberies left is different than being at house 4 with 5 robberies left.
You may be wondering - why don’t we include a state variable that is boolean indicating if we robbed the previous house or not? We certainly could include this state variable, but we can develop our recurrence relation in a way that makes it unnecessary.
The problem is asking for “the maximum amount of money you can rob”. Therefore, we would use either a function dp(i)
that returns the maximum amount of money you can rob up to and including house i
, or an array dp
where dp[i]
represents the maximum amount of money you can rob up to and including house i
.
This means that after all the subproblems have been solved, dp[i]
and dp(i)
both return the answer to the original problem for nums[:i+1]
. To solve the original problem, we will just need to return dp[nums.length-1]
or dp(nums.length-1)
, depending if we do bottom-up or top-down.
2. A recurrence relation to transition between states
Let’s assume we are using a top-down (recursive function) approach. Note that the top-down approach is closer to our natural way of thinking and it is generally easier to think of the recurrence relation if we start with a top-down approach.
We need to find a recurrence relation, which is typically the hardest part of the problem. For any recurrence relation, a good place to start is to think about a general state (in this case, let’s say we’re at the house at index i
), and use information from the problem description to think about how other states relate to the current one.
If we are at some house, logically, we have 2 options: we can choose to rob this house, or we can choose to not rob this house.
- If we decide not to rob the house, then we don’t gain any money. Whatever money we had from the previous house is how much money we will have at this house - which is
dp(i - 1)
. - If we decide to rob the house, then we gain
nums[i]
money. However, this is only possible if we did not rob the previous house. This means the money we had when arriving at this house is the money we had from the previous house without robbing it, which would be however much money we had 2 houses ago,dp(i - 2)
. After robbing the current house, we will havedp(i - 2) + nums[i]
money.
From these two options, we always want to pick the one that gives us maximum profits. Putting it together, we have our recurrence relation: dp(i) = max(dp(i-1), dp(i-2) + nums[i])
.
3. Base cases
The last thing we need is base cases so that our recurrence relation knows when to stop. The base cases are often found from clues in the problem description or found using logical thinking. In this problem, if there is only one house, then the most money we can make is by robbing the house (the alternative is to not rob the house). If there are only two houses, then the most money we can make is by robbing the house with more money (since we have to choose between them). Therefore, our base cases are:
dp(0) = nums[0]
dp(1) = max(nums[0], nums[1])
Top-down implementation
class Solution {
private HashMap<Integer, Integer> memo = new HashMap<Integer, Integer>();
private int[] nums;
private int dp(int i) {
// Base cases
if (i == 0) return nums[0];
if (i == 1) return Math.max(nums[0], nums[1]);
if (!memo.containsKey(i)) {
memo.put(i, Math.max(dp(i - 1), dp(i - 2) + nums[i])); // Recurrence relation
}
return memo.get(i);
}
public int rob(int[] nums) {
this.nums = nums;
return dp(nums.length - 1);
}
}
class Solution:
def rob(self, nums: List[int]) -> int:
def dp(i):
# Base cases
if i == 0:
return nums[0]
if i == 1:
return max(nums[0], nums[1])
if i not in memo:
memo[i] = max(dp(i - 1), dp(i - 2) + nums[i]) # Recurrence relation
return memo[i]
memo = {}
return dp(len(nums) - 1)
Bottom-up implementation
class Solution {
public int rob(int[] nums) {
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]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); // Recurrence relation
}
return dp[nums.length - 1];
}
}
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
dp = [0] * len(nums)
# Base cases
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]) # Recurrence relation
return dp[-1]