【动态规划】打家劫舍
打家劫舍
力扣上打家劫舍相关的题目如下:
序号 | 题目 | 区别 |
---|---|---|
1 | 198. 打家劫舍 | 不能偷窃相邻的房间 |
2 | 213. 打家劫舍 II | 房间连成环 |
3 | 337. 打家劫舍 III | 房间组成一棵二叉树 |
应用
应用1:Leetcode.198
题目
分析
设 \(dp[i]\) 表示前 \(i\) 个房间能获取的最大金额,设房间数量为 \(n\),那么, \(0 \le i \le n - 1\)。
边界条件
当只有一个房间时,显然最大收益,就是选择偷窃该房间所所得的收益,当只有两个房间时,收益就是两个房间的最大值,因此边界条件为:
状态转移
当 \(i \ge 2\) 时,对于第 \(i\) 个房间\(nums[i]\),有两种选择:
-
不选择偷窃这个房间,那么,可以获取的最大金额就是 \(dp[i - 1]\);
-
选择偷窃这个房间,那么,可以获取的最大金额就是 \(dp[i - 2] + nums[i]\);
综上,能获取的最大金额就是上述两种情况的最大值,即状态转移方程:
代码实现
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
int[] dp = new int [n];
dp[0] = nums[0];
dp[1] = Math.max(nums[1], nums[0]);
for (int i = 2; i < n; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[n - 1];
}
}
对其压缩状态,优化后的实现:
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n == 1:
return nums[0]
a = nums[0]
b = max(nums[1], nums[0])
c = b
for i in range(2, n):
c = max(b, a + nums[i])
a = b
b = c
return c
应用2:Leetcode.213
题目
分析
设 \(dp[i]\) 表示前 \(i\) 个房间能获取的最大金额,设房间数量为 \(n\)。
题目中的限制条件,可以等价于:
-
若偷窃了第一个房间,就不能偷窃最后一个房间,即可以偷窃房间的范围为:\(nums[0], \ \cdots, \ nums[n -2]\);
-
若没有偷窃第一个房间,就可以偷窃最后一个房间,即可以偷窃房间的范围为:\(nums[1], \ \cdots, \ nums[n -1]\)。
因此,我们只需要针对上述两种情况,利用前面一道题的分析结果,分别计算两种情况的最大值,最后,再选择最优解即可。
代码实现
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
int[] nums1 = Arrays.copyOfRange(nums, 0, n - 1);
int[] nums2 = Arrays.copyOfRange(nums, 1, n);
return Math.max(doRob(nums1), doRob(nums2));
}
private int doRob(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
int [] dp = new int [n];
dp[0] = nums[0];
dp[1] = Math.max(nums[1], nums[0]);
boolean canUseLastRoom = n % 2 == 0;
for (int i = 2; i < n; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[n - 1];
}
}
应用3:Leetcode.337
题目
分析
由于,我们一般采用递归的方式遍历一棵树,所以,求解树上的动态规划时,通常采用自顶向下的方式,通过分解子问题的方式求解。
对于这道题,我们注意到对于任意一个节点,它都会有两种状态:选择当前节点或者不选择节点。
因此,在遍历二叉树的过程中,为了表示两种状态,我们定一个递归函数:int [] doRob(TreeNode root)
,它会返回一个数组 \([profit_1, profit_2]\),表示当前节点 \(node\) 可以获取的收益,其中数组的值 \(profit[0]\)、\(profit[1]\) 分别表示选择该节点的最大收益和不选择该节点的最大收益。
对于任意一个节点 \(node\),我们都有两种选择:选择当前节点或者不选择节点,因此:
-
选择当前节点,那么,此时的收益就是:当前节点的值,加上不选择两个子节点对应的收益;
-
不选当前节点,那么,此时的收益就是:两个子节点的收益之和。
其中,每一个子节点的最大收益等于,选择该子节点的收益或者不选择该子节点的收益的最大值;
这里,我们选择通过后序遍历的方式计算每个节点收益,即在离开当前节点的时候,计算当前节点的收益。
代码实现
class Solution {
public int rob(TreeNode root) {
int [] profits = doRob(root);
return Math.max(profits[0], profits[1]);
}
private int [] doRob(TreeNode root) {
if (root == null) {
return new int[2];
}
// 左子树的收益
int [] left = doRob(root.left);
// 右子树的收益
int [] right = doRob(root.right);
// 选择当前节点:此时,左右子树都能不能选择
int profit1 = root.val + left[1] + right[1];
// 不选择当前节点:此时,左右子树各自取最大值即可
int profit2 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
return new int [] {profit1, profit2};
}
}