Day42 | 动态规划 :选或不选 打家劫舍&&打家劫舍II
动态规划应该如何学习?-CSDN博客
动态规划学习:
1.思考回溯法(深度优先遍历)怎么写
注意要画树形结构图
2.转成记忆化搜索
看哪些地方是重复计算的,怎么用记忆化搜索给顶替掉这些重复计算
3.把记忆化搜索翻译成动态规划
基本就是1:1转换
198.打家劫舍
198. 打家劫舍 - 力扣(LeetCode)
思路分析:
树形结构图

先明确一下dp数组/dfs函数的含义,dp[i]就是在前i个房子里面打家劫舍,能得到的最高金额(就是题目要求的)
我们从最后一个房子倒着往前分析子问题
对于一个房子i,我们只有两种方案,选或者不选
选了的话,那我i-1就不能选了
不选的话,那我前i个房子可以得到的最大金额数量就和前i-1个房子可以得到的最大金额数量相等,因为第i个房子没选
所以可以很轻易的得出
1
| dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
|
即在可能的两种方案中挑选一个最大值
1.回溯 DFS
1.返回值和参数
i是房子编号
nums是房子数字
dfs返回值就是前i个房子里面打家劫舍可以得到的最大金额(和dp数组含义相同)
1
| int dfs(int i,vector<int>& nums)
|
2.终止条件
可以看到我们递归到0就是叶子结点了,下标0是第一个房子,所以小于0就代表没有房子可以选了,没有房子可以选也就没有金额,就返回0
3.本层逻辑
如上所说,返回两者的最大值给上层递归函数就好
1
| return max(dfs(i-1,nums),dfs(i-2,nums)+nums[i]);
|
完整代码:
当然,这是超时的
1 2 3 4 5 6 7 8 9 10 11 12
| class Solution { public: int dfs(int i,vector<int>& nums) { if(i<0) return 0; return max(dfs(i-1,nums),dfs(i-2,nums)+nums[i]); } int rob(vector<int>& nums) { return dfs(nums.size()-1,nums); } };
|
2.记忆化搜索
就是搞一个哈希表dp,全都初始化为-1,每次返回前给哈希表dp赋值,碰到不是-1的那就是算过的,那就直接返回计算过的结果,不需要再次递归了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Solution { public: int dfs(int i,vector<int>& nums,vector<int>& dp) { if(i<0) return 0; if(dp[i]!=-1) return dp[i]; return dp[i]=max(dfs(i-1,nums,dp),dfs(i-2,nums,dp)+nums[i]); } int rob(vector<int>& nums) { vector<int> dp(nums.size(),-1); return dfs(nums.size()-1,nums,dp); } };
|
3.1:1翻译为动态规划
1.确定dp数组以及下标的含义
dp数组就是前i个房子里面可以取得的最大金额
下标就是房子编号
2.确定递推公式
忘记了原因的请看思路分析部分
1
| dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
|
3.dp数组如何初始化
第一种写法,dp数组的下标从0开始,在i=0和i=1我们的递推公式中会有不合法的状态,所以i需要从2开始
那么dp[0]和dp[1]就需要初始化,那么初始化为多少呢?
如果可以根据dp的含义想通如何初始化是最好的,比如前1间房子最大值就是nums[0],前两间房子最大值就是max(nums[0],nums[1])
如果想不通的话,回到回溯法把0和1带进去就完事得出结果就完事
1 2 3 4 5 6 7 8 9 10 11 12
| class Solution { public: int dfs(int i,vector<int>& nums) { if(i<0) return 0; return max(dfs(i-1,nums),dfs(i-2,nums)+nums[i]); } int rob(vector<int>& nums) { return dfs(nums.size()-1,nums); } };
|
会得到如下的初始化方案:
1 2 3 4 5 6
| vector<int> dp(nums.size(),0); dp[0]=nums[0]; if(nums.size()>1) dp[1]=max(nums[0],nums[1]); else return dp[0];
|
第二种写法
dp数组的下标全部都+2(注意nums数组的下标是没变的,此时dp数组的2对应的是nums的0)
这样就避免了下标是负数的情况,完美贴合了dfs回溯的方法
1 2 3
| vector<int> dp(nums.size()+2,0); for(int i=0;i<nums.size();i++) dp[i+2]=max(dp[i+1],dp[i]+nums[i]);
|
4.确定遍历顺序
后续结果需要依赖前面的计算结果,故使用从前往后遍历
1 2
| for(int i=2;i<nums.size();i++) dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
|
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Solution { public: int rob(vector<int>& nums) { vector<int> dp(nums.size(),0); dp[0]=nums[0]; if(nums.size()>1) dp[1]=max(nums[0],nums[1]); else return dp[0]; for(int i=2;i<nums.size();i++) dp[i]=max(dp[i-1],dp[i-2]+nums[i]); return dp[nums.size()-1]; } };
|
1 2 3 4 5 6 7 8 9
| class Solution { public: int rob(vector<int>& nums) { vector<int> dp(nums.size()+2,0); for(int i=0;i<nums.size();i++) dp[i+2]=max(dp[i+1],dp[i]+nums[i]); return dp[nums.size()+1]; } };
|
213.打家劫舍II
213. 打家劫舍 II - 力扣(LeetCode)
分两种情况,考虑是否偷 第一个房子:
如果偷 nums[0],那么 nums[1] 和 nums[nums.size()−1] 不能偷,问题变成从 nums[2] 到 nums[nums.size()−2] 的非环形版本,直接调打家劫舍的代码
如果不偷 nums[0],那么问题变成从 nums[1] 到 nums[nums.size()−1] 的非环形版本,直接调打家劫舍的代码
当然,如果元素数量<=2的话我们的begin+2直接是不合法的,那就做一个特殊处理就好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Solution { public: int rob1(vector<int> nums) { vector<int> dp(nums.size()+2,0); for(int i=0;i<nums.size();i++) dp[i+2]=max(dp[i+1],dp[i]+nums[i]); return dp[nums.size()+1]; } int rob(vector<int>& nums) { if(nums.size()==1) return nums[0]; if(nums.size()==2) return max(nums[0],nums[1]); return max( nums[0]+rob1(vector<int>(nums.begin()+2,nums.end()-1)), rob1(vector<int>(nums.begin()+1,nums.end()))); } };
|