动态规划解决打家劫舍系列问题
废话少说,直接看题
#### [198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/)
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
思路:最开始的想法是分别统计下标为偶数和奇数之和,然后取最大值,这个遇到这样的就不行了(3,1,2,8),这样的取得是3和8.
这里求最大值问题,肯定是要穷举所有的结果的,自然而然想到使用动态规划,动态规划第一步明确定义dp[i],这里为偷到第i家之前最多偷的钱。
第二步,写转移方程,我们知道此时投的钱之后两种可能,要么是不偷这家,dp[i]=dp[i-1],要么是偷了这家,加上之前偷的,之前的肯定是dp[i-2]了。
即dp[i]=max(dp[i-1],dp[i-2]+nums[i])
这里为了避免讨论边界,可以取dp的长度为nums.length+1
代码如下:
public int rob(int[] nums) {
int n=nums.length;
if(n<1) return 0;
int[] dp=new int[nums.length+1];
dp[0]=0;
dp[1]=nums[0];
// dp[1]=Math.max(nums[0],nums[1]);
for(int i=2;i<=n;i++){
dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i-1]);
}
return dp[n];
}
我们在仔细看一下代码,其实发现dp[n] 只与 dp[n-1]和 dp[n−2] 有关系,因此我们可以设两个变量 cur和 pre 交替记录,将空间复杂度降到 O(1)。
代码如下
public int rob(int[] nums){
int pre,cur,temp;
for(int num:nums){
temp=cur;
cur=Math.max(cur,pre+num);
pre=temp;
}
return cur;
}
#### [213. 打家劫舍 II](https://leetcode-cn.com/problems/house-robber-ii/)
第二题和第一题的区别就是首位两家不能同时偷,其实和第一题一个思路,不过变成了其实就是求偷0.n-1家和1,n家的最大值
代码如下:
class Solution {
public int rob(int[] nums) {
if(nums.length<1) return 0;
if(nums.length==1) return nums[0];
return Math.max(rob11(Arrays.copyOfRange(nums,0,nums.length-1)),rob11(Arrays.copyOfRange(nums,1,nums.length)));
}
public int rob11(int[] nums) {
int n=nums.length;
if(n<1) return 0;
// if(nums.length==1) return nums[0];
int[] dp=new int[nums.length+1];
dp[0]=0;
dp[1]=nums[0];
// dp[1]=Math.max(nums[0],nums[1]);
for(int i=2;i<=n;i++){
dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i-1]);
}
return dp[n];
}
}
先将一题和这个相似的。
这题的重点是删除所有等于nums[i] - 1
或 nums[i] + 1
的元素,就拿例题来看[2,2,3,3,3,4,4],从这个来看,如果取3,那么所有的2和4都会没有,那么就变成了[0,0,3,3,0,0],这时肯定还会取3,这就说明了一旦决定选择一个数,那么所有相同的数都要被选择。这个例题就变成了这样【2+2,3+3+3,4+4】取最大,这就是上面的打家劫舍问题了。
代码如下
class Solution {
public int deleteAndEarn(int[] nums) {
int[] count=new int[10001];
for(int i=0;i<nums.length;i++){
count[nums[i]]+=nums[i];
}
int[] dp=new int[count.length+1];
dp[0]=0;
dp[1]=count[0];
for(int i=2;i<dp.length;i++){
dp[i]=Math.max(dp[i-1],dp[i-2]+count[i-1]);
}
return dp[dp.length-1];
}
}
#### [337. 打家劫舍 III](https://leetcode-cn.com/problems/house-robber-iii/)
这个题除了名字和上面的相同,其他什么都不样。
不过遇到这个题我首先的思路是先用层序遍历将每层的数值叠加,变成一个数组,然后对数组进行打家劫舍1的操作,代码是这个样子的
class Solution {
public int rob(TreeNode root) {
if(root==null) return 0;
Queue<TreeNode> queue=new LinkedList<>();
int sum1=0,sum2=0;
int deepth=0;
queue.add(root);
while(!queue.isEmpty()){
deepth++;
int n=queue.size();
for(int i=0;i<n;i++){
TreeNode node=queue.poll();
if(deepth%2==0){
sum1+=node.val;
}else{
sum2+=node.val;
}
if(node.left!=null) queue.add(node.left);
if(node.right!=null) queue.add(node.right);
}
}
return Math.max(sum1,sum2);
}
}
124个测试用理过了61个,说明肯定不是边界问题。
后来才知道,如果遇到这样的
```
1
/ \
2 3
\
3
```
这个显然取两个3,这个方法就无效了。应该使用dfs来做。明确这里的rob(root)含义:以root为根可获得的最大值。就避免了上面的问题。
解法如下:
class Solution {
public int rob(TreeNode root) {
if(root==null) return 0;
int money=root.val;
if(root.left!=null){
money+=(rob(root.left.left)+rob(root.left.right));
}
if(root.right!=null){
money+=(rob(root.right.left)+rob(root.right.right));
}
return Math.max(money,rob(root.left)+rob(root.right));
}
}
这里存在重复计算问题,增加一个备注
class Solution {
public int rob(TreeNode root) {
HashMap<TreeNode, Integer> memo = new HashMap<>();
return robInternal(root, memo);
}
public int robInternal(TreeNode root, HashMap<TreeNode, Integer> memo) {
if (root == null) return 0;
if (memo.containsKey(root)) return memo.get(root);
int money = root.val;
if (root.left != null) {
money += (robInternal(root.left.left, memo) + robInternal(root.left.right, memo));
}
if (root.right != null) {
money += (robInternal(root.right.left, memo) + robInternal(root.right.right, memo));
}
int result = Math.max(money, robInternal(root.left, memo) + robInternal(root.right, memo));
memo.put(root, result);
return result;
}
}