除了背包系列问题,打家劫舍系列问题同样也是动态规划的经典题目。在这篇文章中荔枝将会把打家劫舍系列问题做一下总结,再仔细体会动态规划的思想,希望能帮到有需要的小伙伴~~~
前言
一、Leecode198.打家劫舍
1.1 分析
1.2 题解示例
二、Leecode213.打家劫舍II
2.1 分析
2.2 题解示例
三、Leecode337.打家劫舍III
3.1 分析
3.2 题解示例
总结
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
输入样例:
[1,2,3,1]
输出样例:
4
明确dp数组含义和递推式
从题目的描述中我们可以了解到,相邻的房子因为有防盗系统是不能偷的,所以当前房屋偷与不偷是由前面的两个房子来决定的。弄清楚这个关系之后我们首先需要明确dp数组的含义dp[i]:考虑包括i的房屋在内最多可以偷窃的金额。第i个房屋有两个状态:偷和不偷,偷的时候dp[i] = dp[i-1]+nums[i],不偷的话就是前一个房屋被偷了dp[i] = dp[i-1],我们要取最大偷窃金额的话就需要比较这两个状态的大小并取最大值,这时候就得到了递推公式:
dp[i] = max(dp[i-2]+nums[i],dp[i-1]);
确定初始化条件和遍历顺序
我们看到dp[i]其实是由前两个状态共同决定的,因此我们需要初始化dp[0]和dp[1]。根据dp数组的定义,dp[0] = nums[0]。由于一开始的两个房屋我们只能偷一个并让金额取得最大值,所以再i=1的时候dp[1] = max(nums[0],nums[1])。其余的下标可以初始化成一个最小的非负整数,所以初始化为0即可。最后返回的结果就是取到nums数组的最后一个元素时取到的最小金额dp[nums.size()-1]。
class Solution {
public:
int rob(vector& nums) {
if(nums.size()==1) return nums[0];
vector dp(nums.size(),0);
dp[0] = nums[0];
dp[1] = max(nums[0],nums[1]);
for(int i=2;i
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/house-robber
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,今晚能够偷窃到的最高金额。
输入样例:
nums = [2,3,2]
输出样例:
3
打家劫舍II和打家劫舍最大的的区别就是房屋围成了一圈,这样子首尾房屋就是相邻的房屋,这就会出现三种情况:偷首不偷尾、偷尾不偷首、首尾皆不偷。要想较好的来解决成环后的问题,我们首先根据这三种情况来将成环后的房屋转换成不成环的打家劫舍问题。由于第三种情况包含在前两种情况中,也就是说,我们可以将题目给出的房屋价值数组分为两类:0到nums.size()-2和1到nums.size()-1。
分析到这里,其实这道题目就转化成了Leecode198.打家劫舍问题了,本题的重点是如何将环打开以及明确打开环后的三种情况,弄懂了上面的打家劫舍问题荔枝相信这道题目还是比较容易AC出来的。
class Solution {
public:
int rob(vector& nums) {
if(nums.size()==1) return nums[0];
//两种情况——考虑左节点和右节点
int left = robb(nums,0,nums.size()-2);
int right = robb(nums,1,nums.size()-1);
return max(left,right);
}
int robb(vector& nums,int start,int end){
if(start==end) return nums[start];
vector dp(nums.size());
dp[start] = nums[start];
dp[start + 1] = max(nums[start], nums[start + 1]);
for(int i=start+2;i<=end;i++){
dp[i] = max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[end];
}
};
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/house-robber-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题目描述:
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
输入样例:
root = [3,2,3,null,3,null,1]
输出样例:
7
明确dp数组的含义
首先我们确定一下dp数组及其对应的下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。由于这道题目和二叉树结合在一起,所以我们借助二叉树的性质确定边界条件:遇到空节点就会返回0。
确定遍历顺序
由于这道题目是需要通过递归函数的返回值来做下一步的判断和计算的,所以这里采用的是后序遍历的方式来遍历。
单层递归过程
在递归函数中,我们采用后序遍历的方式将左右节点分别求其偷与不偷的结果集,并对当前节点分成两类情况来分析,偷父节点就不能偷子节点,存储偷与不偷的结果并最后返回求最大值即可。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int rob(TreeNode* root) {
vector result = robTree(root);
return max(result[0], result[1]);
}
//[0]:不偷能够盗取到的最高金额
//[1]:偷能够盗取到的最高金额
//robTree会返回一个偷与不偷的结果集
vector robTree(TreeNode* cur) {
if (cur == NULL) return vector{0, 0};
vector left = robTree(cur->left);
vector right = robTree(cur->right);
// 偷cur,那么就不能偷左右节点。
int val1 = cur->val + left[0] + right[0];
// 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
}
};
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/house-robber-iii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
我们看到其实打家劫舍系列问题中难度其实是在逐步递增的,需要注意的其实就是打家劫舍III,它跳出了动态规划的固有思维,和二叉树的求解方法融合在了一起,确实也比较难想到求解的方式。荔枝感觉其实动态规划更像是一个黑箱,你可以只需要给出dp推导式和遍历顺序,初始化后就可以干活了。难度还是很大的,也可能是因为荔枝正处于初级阶段吧哈哈哈哈哈。总的来说打家劫舍III还是需要好好注意一下这种思维方式,当然了后续在股票投资问题也是会有相应的处理过程哈哈。
在这篇文章中,荔枝把动态规划中地打家劫舍系列问题总结完成,个人感觉刷完题目再来写总结Blog确实收获很大,之前不太清晰的地方也渐渐地弄清楚了,大家继续跟荔枝一起加油吧哈哈哈哈哈~~~
今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~