你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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
首先,dp数组是我们求什么,dp数组含义就是什么,因此本题dp数组含义是考虑下标i以内的房屋,能偷到的最大金额是dp[i]。(注意是考虑下标i以内房屋,而不是就只偷下标i的房屋)
决定dp[i]
的因素就是第i个房间偷还是不偷。
dp[i] = dp[i - 2] + nums[i]
,即:第i-1房一定是不考虑的(会触发警报),找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。i-1
的房间,此时的最大价值就是dp[i-1]
最后dp[i]
取最大值,也就是所求的最高金额,递推公式:
dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
求最大值类型,初始化全部为0,dp[0]意思并不是考虑0个房屋,金额最大就是0,而是考虑下标为0的房屋,金额应该是nums[0]!
初始化一般是考虑递推公式数组越界的问题,因此i=0和i=1都需要初始化。dp[0]=0,dp[1]就是偷窃第1个房屋的数值nums[1]。所以dp[1]=max(dp[0],dp[0]+nums[1])
dp[i] 是根据dp[i - 2]
和 dp[i - 1]
推导出来的,那么一定是从前到后遍历
dp[0]=nums[0]
!class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==0) return 0;
if(nums.size()==1){
return nums[0];
}
//dp[i]考虑下标i以内的房屋,偷到的最大价值
vector<int>dp(nums.size()+1,0);
//初始化
//初始化问题,dp[0]的意思是考虑下标0的房屋,下标0的房屋就是第一间房屋!
//dp[0]不能=0,而应该=nums[0]!
dp[0]=nums[0];
dp[1]=max(dp[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];
}
};
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 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
本题和 打家劫舍 很像,打家劫舍Ⅰ,是给了一个普通数组nums[i],相邻的房间不能偷。
而打家劫舍Ⅱ是把数组连成环,体现在数组里,就是数组第一个元素与最后一个元素相邻,如果选了第一个元素,就不能选最后一个元素。
线性数组连成环状,最后一个元素和第一个元素相邻,首尾不能同时选择。实际上,首尾元素不能同时选,可以直接分为三种情况:
0--nums.size()-2
1--nums.size()-1
但是实际上,首尾元素都不选的情况,是被包含在只选首元素/只选尾部元素的情况里面的。如下图所示:
首尾元素只选首元素:
第一种情况将首元素和中间部分都考虑了,那么中间部分取得的最优值,实际上已经包含在第一种情况里面了!
因此我们只需要考虑不选首元素和不选尾部元素的情况就可以。
由于我们只有这两种情况,其余情况都与打家劫舍Ⅰ相同,我们可以将打家劫舍Ⅰ的部分封装成函数,输入不同的数据范围。
//非环形数组的情况,输入开始和结束下标
int robRange(vector<int>&nums,int start,int end){
vector<int>dp(nums.size()+1,0);
//初始化
dp[start]=nums[start];
dp[start+1]=max(nums[start],nums[start+1]);//不能偷相邻的,因此直接是两个nums[]对比
//为了防止start=0的情况需要从start+2开始
for(int i=start+2;i<=end;i++){
dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[end];
}
class Solution {
public:
int rob(vector<int>& nums) {
//处理没有dp[0]和dp[1]的特殊情况
if(nums.size()==0) return 0;
if(nums.size()==1) return nums[0];
int result=0,result1=0,result2=0;
result1 = robRange(nums,0,nums.size()-2);
result2 = robRange(nums,1,nums.size()-1);
result = max(result1,result2);
return result;
}
int robRange(vector<int>&nums,int start,int end){
vector<int>dp(end-start+1,0);
//初始化
dp[start]=nums[start];
dp[start+1]=max(nums[start],nums[start+1]);//不能偷相邻的,因此直接是两个nums[]对比
//为了防止start=0的情况需要从start+2开始
for(int i=start+2;i<=end;i++){
dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[end];
}
};
这种写法,如果我们写成dp数组为end-start+1
,那么如果原数组的下标范围i
是1~nums.size()-1
,那么dp数组的长度就是num.size()-1-1+1=nums.size()-1,也就是说下标范围是**0~nums.size()-2
**!
因此,在dp数组内部,初始化的时候可以直接用dp[0]和dp[1]。
初始化修改:
//dp数组的内部下标是0--nums.size()-2!
dp[0]=nums[start];
dp[1]=max(nums[start],nums[start+1]);
递推公式修改:
for(int i=2;i<=end-start;i++){//end是nums.size()-1,但是dp[i]最大是dp[nums.size()-2]!
dp[i]=max(dp[i-1],dp[i-2]+nums[i+start]);
}
return dp[end-start];
0~nums.size()-2
!class Solution {
public:
int rob(vector<int>& nums) {
//处理没有dp[0]和dp[1]的特殊情况
if(nums.size()==0) return 0;
if(nums.size()==1) return nums[0];
int result=0,result1=0,result2=0;
result1 = robRange(nums,0,nums.size()-2);
result2 = robRange(nums,1,nums.size()-1);
result = max(result1,result2);
return result;
}
int robRange(vector<int>&nums,int start,int end){
if(end==start) return nums[start];
vector<int>dp(end-start+1,0);
//初始化
dp[0]=nums[start];
dp[1]=max(nums[start],nums[start+1]);//不能偷相邻的,因此直接是两个nums[]对比
//为了防止start=0的情况需要从start+2开始
for(int i=2;i<=end-start;i++){
dp[i]=max(dp[i-1],dp[i-2]+nums[i+start]);
}
return dp[end-start];
}
};
class Solution {
public:
int rob(vector<int>& nums) {
//处理没有dp[0]和dp[1]的特殊情况
if(nums.size()==0) return 0;
if(nums.size()==1) return nums[0];
int result=0,result1=0,result2=0;
result1=robRange(nums,0,nums.size()-2);
result2 = robRange(nums,1,nums.size()-1);
result = max(result1,result2);
return result;
}
//非环形数组的情况,输入开始和结束下标
int robRange(vector<int>&nums,int start,int end){
if(start==end) return nums[start];
vector<int>dp(nums.size()+1,0);
//初始化
dp[start]=nums[start];
dp[start+1]=max(nums[start],nums[start+1]);//不能偷相邻的,因此直接是两个nums[]对比
//为了防止start=0的情况需要从start+2开始
for(int i=start+2;i<=end;i++){
dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[end];
}
};
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 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, 10^4]
范围内0 <= Node.val <= 10^4
本题从环形数组进阶成了二叉树,只要是线相邻在一起,就不能偷。
本题是树形DP的一种,一般的DP是在线性数组/环形数组中进行状态的转移,而树形DP是在二叉树中进行状态的转移。
本题的暴力解法是记忆化递归,也就是用memo数组来记忆已经遍历过的点,记忆化递归在 343.整数拆分 里面用到过。
这是一个二叉树的结构,每个节点也只有两个状态,就是偷和不偷。
我们可以用一个长度为2的一维DP数组来表示当前节点的状态,下标为0表示不偷,下标为1表示偷。
遍历二叉树的过程,我们使用递归去遍历,系统栈里面会保存每一层递归的参数。每一层递归(对应每个节点)里面,其实都有一个长度为2的DP数组。
因此,当前层的DP数组,就表示当前节点的状态。不需要再去定义每个节点的DP数组。
对于每层的节点,dp[0]
表示不偷当前节点所获得的最大金钱,dp[1]
表示偷当前节点所获得的最大金钱。
vector<int>robTree(TreeNode* root){
//终止条件,遇到空节点,直接返回{0,0}数组,相当于初始化
if(root==null) return vector<int>{0,0};
}
vector<int>robTree(TreeNode* root){
//终止条件,遇到空节点,直接返回{0,0}数组,相当于初始化
if(root==null) return vector<int>{0,0};
//单层递归
//如果偷当前节点,那么左右孩子都不偷得到的最大金额,left[0]是递归后序遍历得到的
int value1 = root->val+leftdp[0]+rightdp[0];
}
因为需要得到左孩子不偷,也就是left[0]
(DP数组含义,left[0]是该状态(不偷)下的金额最大值)的数值,因此必须是后序遍历,一层层将DP状态向上返回。
int value2 = max(leftdp[0],leftdp[1])+max(rightdp[0],rightdp[1]);
vector<int>robTree(TreeNode* root){
//终止条件,遇到空节点,直接返回{0,0}数组,相当于初始化
if(root==null) return vector<int>{0,0};
//单层递归
//左孩子dp数组dp[0]dp[1]取值
vector<int>leftdp = robTree(root->left);
vector<int>rightdp = robTree(root->right);//得到左孩子和右孩子,偷与不偷的最大值
//如果偷当前节点,那么左右孩子都不偷得到的最大金额,left[0]是递归后序遍历得到的
int value1 = root->val+leftdp[0]+rightdp[0];
//如果不偷当前节点,左右孩子都可能偷,取它们分别的最大值相加
int value2 = max(leftdp[0],leftdp[1])+max(rightdp[0],rightdp[1]);
//返回值:返回这个节点的DP数组,偷是value1,不偷是value2
return {value1,value2};
}
以给出的二叉树为例,树形DP数组如下图所示:
需要注意:偷根节点的时候,左右两边都必须是不偷的状态;但是不偷根节点的话,左右两边可偷可不偷,直接找两边DP数组的最大值相加即可!
树形DP的特点就是每一层递归都是一个节点,每个节点对应一个DP数组!向上返回的是本层节点的DP数组!
不同的状态由DP数组的下标来表示,下标0表示偷对应的DP值,下标1表示不偷对应的DP值。
可以上面打印的DP数组来理解。
class Solution {
public:
vector<int>travelsal(TreeNode* root){
//终止条件
if(root==nullptr) return vector<int>{0,0};//相当于初始化
//单层递归,左右中,后序遍历
vector<int>leftdp = travelsal(root->left);
vector<int>rightdp = travelsal(root->right);
//中,递推部分
//value1代表偷,意味着左右都不能偷
int value1 = root->val+leftdp[1]+rightdp[1];
//不偷,左右可偷可不偷,选最大值
int value2 = max(leftdp[0],leftdp[1])+max(rightdp[0],rightdp[1]);
return {value1,value2};
}
int rob(TreeNode* root) {
vector<int>res = travelsal(root);
//返回的是根节点的{value1,value2},需要再次判断根节点偷不偷
return max(res[0],res[1]);
}
};
本题属于树形DP,所谓树形DP就是在树上进行递归公式的推导,每层递归对应一个节点,每个节点对应一个DP数组。DP数组下标0和1代表当前节点的状态,dp[0]和dp[1]是当前节点不同状态对应的DP值。
本题就是二叉树在动态规划中的应用。968.监控二叉树 这道题,是二叉树在贪心中的运用。