动态规划day39|198. 打家劫舍、213. 打家劫舍 II(环形怎么处理?)、337. 打家劫舍 III(二叉树与动态规划的完美结合!)
- 198. 打家劫舍
- 213. 打家劫舍 II
- 337. 打家劫舍 III
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
class Solution {
public:int rob(vector<int>& nums) {vector<int> dp(nums.size(),0);if(nums.size()==0) return 0;if(nums.size()==1) return nums[0];dp[0]=nums[0];dp[1]=max(nums[0],nums[1]);for(int i=2;i<nums.size();i++)dp[i]=max(dp[i-2]+nums[i],dp[i-1]);return dp[nums.size()-1];}
};
动规五部曲:
-
dp数组的含义:dp[i]表示在前i家房间里面,所能偷窃的最大数值
-
初始化:
dp[0]=nums[0];
dp[1]=max(nums[0],nums[1]);
对0 和1 显然都要初始化,由统一的思路和dp数组的含义来思考初始化的值
- 递推公式:
dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
反映了前后项的关系,当不偷第i家时,dp[i]=dp[i-1];当偷第i家时,dp[i]=dp[i-2]+nums[i](动规的老套路了)。然后取两者中的最大值即可。
-
遍历顺序:从小到大
-
检验
注意:
if(nums.size()==0) return 0;
if(nums.size()==1) return nums[0];
对数量只有0和1的情况要做剪枝操作。因为初始化是建立在dp[0]、dp[1]都存在的基础上的,当size()=0时dp[0]、dp[1]都不存在;当size()=1时,dp[1]不存在。所以这两种情况要单独考虑。
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3]
输出:3
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
class Solution {
public:int rob1(vector<int>& nums){if(nums.size()==0) return 0;if(nums.size()==1) return nums[0];vector<int> dp(nums.size(),0);dp[0]=nums[0];dp[1]=max(nums[0],nums[1]);for(int i=2;i<nums.size();i++)dp[i]=max(dp[i-1],dp[i-2]+nums[i]);return dp[nums.size()-1];}int rob(vector<int>& nums) {if(nums.size()==0) return 0;if(nums.size()==1) return nums[0];vector<int> nums1(nums.begin(),nums.begin()+nums.size()-1);vector<int> nums2(nums.begin()+1,nums.end());int num1=rob1(nums1);int num2=rob1(nums2);return num1>num2?num1:num2;}
};
处理环形,我们的思路一般是:通过对首尾元素的取舍,把环形变成线形,现在分三种情况:
- 首尾都不包括
- 包括首,除去尾
- 除去首,包括尾
我们可以发现,情况二三是包括情况一的,所以我们只需要讨论情况二三即可。思路是对原数组进行截取,把截取到的区间代到198.打家劫舍的代码中即可。
337. 打家劫舍 III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:
输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
提示:
- 树的节点数在
[1, 104]
范围内 0 <= Node.val <= 104
class Solution {
public:int rob(TreeNode* root) {vector<int> result=robTree(root);return max(result[0],result[1]);}vector<int> robTree(TreeNode*root){if(root==nullptr) return vector<int>{0,0};vector<int> left=robTree(root->left);vector<int> right=robTree(root->right);vector<int> dp(2,0);dp[0]=max(left[0],left[1])+max(right[0],right[1]);dp[1]=root->val+left[0]+right[0];return dp;}
};
需要火速复习一下二叉树了,二叉树的遍历明显开始遗忘了。
难点:
- 后序遍历:因为对当前结点的操作需要用到孩子结点的信息
- dp数组的含义:
- dp[0]表示当前结点不偷,可以获得的总的最大值
- dp[1]表示偷了当前结点,可以获得的总的最大值
- 递归函数的作用:递归函数其实返回的是任意一个结点在偷和不偷两个状态下分别所获得的最大值,所以它是以一个动态数组的形式返回。
- 为什么根节点代表最终值:其实这个不要太过考虑,因为对树的遍历的逻辑里,自然是根为最终结果的,只要按照正常遍历的逻辑去做,就不会有问题。