198. 打家劫舍
解法一
dp数组:
dp[i][0]
:不偷下标为 i 的房屋时,偷窃前 i 个房屋能获得的最大金额dp[i][1]
:偷下标为 i 的房屋时,偷窃前 i 个房屋能获得的最大金额
最终结果:max(dp[n-1][0], dp[n-1][1])
在计算dp[i][x]
时:
dp[i][0]
:问题转换为偷窃前 i-1 个房屋能获得的最大金额:dp[i][0] = max(dp[i-1][0], dp[i-1][1])
dp[i][1]
:- 此时不能偷第 i-1 个房屋
- 可以先在前 i-1 个房屋中进行偷窃,但不偷第 i-1 个房屋,然后再偷第 i 个房屋:
dp[i][1] = dp[i-1][0] + nums[i]
递推公式:
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i];
递推公式适用范围:令递推公式中所有下标都大于等于0小于n,即i >= 0, i-1 >= 0, i < n, i-1 < n
,计算得到:1 <= i < n
计算顺序:在计算dp[i][x]
时需要用到dp[i-1][x]
,因此应该从前向后计算i = 0 -> n-1
初始化:
可以使用递推公式进行计算的范围是1 <= i < n
,因此需要对 i = 0
的情况进行初始化
class Solution {
public int rob(int[] nums) {
int n = nums.length;
// dp[i][0]:不偷下标为 i 的房屋时,偷窃前 i 个房屋能获得的最大金额
// dp[i][1]:偷下标为 i 的房屋时,偷窃前 i 个房屋能获得的最大金额
int[][] dp = new int[n][2];
// 初始化
dp[0][0] = 0;
dp[0][1] = nums[0];
// 递推计算
for(int i = 1; i < n; i++){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i];
}
// 返回结果
return Math.max(dp[n-1][0], dp[n-1][1]);
}
}
解法二
这是解法一的递推公式:
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i];
对于其中的第二个式子dp[i][1] = dp[i-1][0] + nums[i]
,如果将dp[i-1][0]
代换成Math.max(dp[i-2][0], dp[i-2][1])
,则递推公式变为:
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]);
dp[i][1] = Math.max(dp[i-2][0], dp[i-2][1]) + dp[i-1][0] + nums[i];
可以使用递推公式进行计算的范围也从1 <= i < n
变为2 <= i < n
。
观察这个递推公式发现,其实不用单独区分dp[i][0]
和dp[i][1]
,可以直接将dp[i]
定义为:在下标小于等于i的房屋中偷窃,能得到的最大金额。
在计算dp[i]时:
- 可以选择偷第 i 个房屋:
dp[i] = dp[i-2] + nums[i]
- 也可以选择不偷第 i 个房屋:
dp[i] = dp[i-1]
- 从两者中取最大值
递推公式:
dp[i] = Math.max(dp[i-1], nums[i] + dp[i-2])
class Solution {
public int rob(int[] nums) {
int n = nums.length;
// dp[i]:在下标小于等于i的房屋中偷窃,能得到的最大金额
int[] dp = new int[n];
// 初始化
dp[0] = nums[0];
if(n == 1){
return dp[0];
}
dp[1] = Math.max(nums[0], nums[1]);
if(n == 2){
return dp[1];
}
// 递推计算
for(int i = 2; i < n; i++){
// 选择一:不偷窃第i个房屋 => dp[i] = dp[i-1]
// 选择二:偷窃第i个房屋 => dp[i] = nums[i] + dp[i-2]
dp[i] = Math.max(dp[i-1], nums[i] + dp[i-2]);
}
// 返回结果
return dp[n-1];
}
}
这两种解法都可以在空间复杂度上进行进一步的优化。
213. 打家劫舍 Ⅱ
对是否偷窃nums[0]
进行分类讨论:
- 偷窃
nums[0]
:此时不能偷窃nums[1]
和nums[n-1]
,问题退化为在nums[2...n-2]
上的非环形情况 - 不偷窃
nums[0]
:此时问题退化为在nums[1...n-1]
上的非环形情况 - 两者取最大值
非环形情况下的求解同198. 打家劫舍
class Solution {
public int rob(int[] nums) {
int n = nums.length;
/* 对是否偷窃nums[0]进行分类讨论:
* ① 偷窃nums[0]:此时不能偷窃nums[1]和nums[n-1],问题退化为在nums[2...n-2]上的非环形情况
* ② 不偷窃nums[0]:此时问题退化为在nums[1...n-1]上的非环形情况
* 两者取最大值
*/
return Math.max(rob1(nums, 2, n-2) + nums[0], rob1(nums, 1, n-1));
}
// 在nums[start...end]上的非环形情况
private int rob1(int[] nums, int start, int end){
if(start > end){
return 0;
}
// dp[i]: 在下标小于等于i的房屋中偷窃,能得到的最大金额
int[] dp = new int[end+1];
// 初始化
dp[start] = nums[start];
if(start == end){
return dp[start];
}
dp[start+1] = Math.max(nums[start], nums[start+1]);
if(start+1 == end){
return dp[start+1];
}
// 递推计算
for(int i = start+2; i <= end; i++){
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
}
// 返回结果
return dp[end];
}
}
337. 打家劫舍 Ⅲ
这是一道树形动态规划的题目,多阶段决策对应于树中的一个节点,此时dp应该用哈希表来存。
dp定义:
dp0(v)
:在不偷盗节点v的情况下,在以v为根的子树上进行偷盗能获取的最大金额dp1(v)
:在偷盗节点v的情况下,在以v为根的子树上进行偷盗能获取的最大金额
最终结果:max(dp0(root), dp1(root))
在计算dpx(v)
时:
dp1(v)
:此时不能偷盗v的左右孩子节点 l 和 rdp1(v) = dp0(l) + dp0(r) + v.val
dp0(v)
:此时可以选择偷左孩子 l 或者不偷左孩子 l,也可以选择偷右孩子 r 或者不偷右孩子 rdp0(v) = max(dp0(l), dp1(l)) + max(dp0(r), dp1(r))
计算顺序:在计算节点v的dp值时,需要用到它的左孩子和右孩子 l 和 r 的dp值;因此需要先计算左右孩子的dp值,再计算节点v的dp值 => 后序遍历
class Solution {
public int rob(TreeNode root) {
if(root == null){
return 0;
}
postorder(root);
return Math.max(dp0.get(root), dp1.get(root));
}
// dp0(v):在不偷盗节点v的情况下,在以v为根的子树上进行偷盗能获取的最大金额
// dp1(v):在偷盗节点v的情况下,在以v为根的子树上进行偷盗能获取的最大金额
Map<TreeNode, Integer> dp0 = new HashMap<>();
Map<TreeNode, Integer> dp1 = new HashMap<>();
private void postorder(TreeNode v){
if(v == null){
dp0.put(v, 0);
dp1.put(v, 0);
return;
}
postorder(v.left);
postorder(v.right);
// 递推计算
int dp0_l = dp0.get(v.left);
int dp0_r = dp0.get(v.right);
int dp1_l = dp1.get(v.left);
int dp1_r = dp1.get(v.right);
dp0.put(v, Math.max(dp0_l, dp1_l) + Math.max(dp0_r, dp1_r));
dp1.put(v, dp0_l + dp0_r + v.val);
}
}
空间复杂度优化
在上面的方法中,我们是按照 “先左孩子,后右孩子,最后自己(l -> r => v
)” 的顺序进行的。
观察递推公式发现:在计算节点 v 的dp值时,只会用到自己的左孩子和右孩子的 dp 值,而且当计算完节点 v 的dp值之后,就再也用不到它的左孩子和右孩子的 dp 值。
因此我们可以将每个节点的 dp 值作为后序遍历递归函数的返回值传给父节点,从而省去了两个哈希表的存储空间。
class Solution {
public int rob(TreeNode root) {
if(root == null){
return 0;
}
int[] result = postorder(root);
return Math.max(result[0], result[1]);
}
// dp0(v):在不偷盗节点v的情况下,在以v为根的子树上进行偷盗能获取的最大金额
// dp1(v):在偷盗节点v的情况下,在以v为根的子树上进行偷盗能获取的最大金额
// 返回值:[dp0(v), dp1(v)]
private int[] postorder(TreeNode v){
if(v == null){
return new int[] {0, 0};
}
int[] l = postorder(v.left);
int[] r = postorder(v.right);
// 递推计算
int dp0_v = Math.max(l[0], l[1]) + Math.max(r[0], r[1]);
int dp1_v = l[0] + r[0] + v.val;
return new int[] {dp0_v, dp1_v};
}
}