你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:输入: [2,7,9,3,1]
输出: 12
动态规划的题目,具有以下三个特点
备忘录方法(动态规划的变形)
当我们在求解过程中,其实有很多子问题是不会出现的,我们没必要求出所有的子问题的最优解
我们可以为每个子问题建立一个记录项,在第一次计算时做记录,以后直接读取
对于这道题来说,我们首先肯定想到直接用递归做
int rob(vector<int>& nums)
{
return robber(nums,0);
}
int robber(vector<int> &nums,int house)
{
if(nums.size()-house<=0)
return 0;
if(nums.size()-house==1)
return nums[house];
return max(robber(nums,house+2)+nums[house],robber(nums,house+1));
}
但是这样是指数式的时间复杂度,必然会超时,因此我们考虑用带备忘录的动态规划,在上面的代码中稍微修改一下。
因为对于每次递归,都需要
return max(robber(nums,house+2)+nums[house],robber(nums,house+1));
即如果每次涉及到这一步,都需要再将这个问题的子问题全部重新计算一遍,耗费了大量时间,因此我们可以选择直接将这个max值储存起来,每次需要时再进行调用。
即开一个数组,对于每一个house值
wealth[house]=max(robber(nums,house+2)+nums[house],robber(nums,house+1));
所以有了下面的方法:
int rob(vector<int>& nums)
{
int *wealth=new int[nums.size()];
for(int i=0;i<nums.size();++i)
wealth[i]=-1;
return robber(nums,0,wealth);
}
int robber(vector<int> &nums,int house,int *wealth)
{
if(nums.size()-house<=0)
return 0;
if(nums.size()-house==1)
return nums[house];
if(wealth[house]!=-1)
return wealth[house];
else
{
wealth[house]=max(robber(nums,house+2,wealth)+nums[house],robber(nums,house+1,wealth));
return wealth[house];}
}
如果我们不用递归呢?直接通过循环可以解决吗?
当然可以。
分析上面的代码,可以得出状态转移方程:
f ( k ) = m a x ( f ( k – 2 ) + x , f ( k – 1 ) ) f(k) = max(f(k – 2) + x, f(k – 1)) f(k)=max(f(k–2)+x,f(k–1))
也即要么取第k个房子里的x财物,然后从k+2的房子开始重新考虑,要么不取这个房子里的财物,从k+1开始考虑
所以有:
int rob(vector<int> &nums) {
int p = 0; //代表考虑偷某个房子之前手里的财物
int q = 0; //代表考虑完后手里的财物
for (int i=0;i<nums.size();++i) {
int temp = q;
q = max(p + nums[i], q); //考虑分为两种情况,选择收益更大的一种
p = temp;
}
return q;
}
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
注意,这题和前面不同之处在于这句话:这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。
也就意味着,第一间房屋和最后一间房屋,最多只能选择一间偷窃。
因此我们可以分为两种情况:
然后将两种情况的最大值进行比较,得出的最大值就是最高金额。
把上面基于备忘录的代码稍事修改,对两种情况分别设置备忘录进行储存,有了如下方案:
int rob(vector<int>& nums)
{
int *wealth1=new int[nums.size()];
int *wealth2=new int[nums.size()];
if(nums.size()==1) return nums[0];
for(int i=0;i<nums.size();++i)
{
wealth1[i]=-1;
wealth2[i]=-1;
}
return max(robber(nums,1,nums.size(),wealth1),robber(nums,0,nums.size()-1,wealth2));
}
int robber(vector<int> &nums,int house,int end,int *wealth)
{
if(end-house<=0)
return 0;
if(end-house==1)
return nums[house];
if(wealth[house]!=-1)
return wealth[house];
else
{
wealth[house]=max(robber(nums,house+2,end,wealth)+nums[house],robber(nums,house+1,end,wealth));
return wealth[house];
}
}
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 :
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
/ \
3 1输出: 7
小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
换汤不换药,依然是相似的题目,不过采用了二叉树式的排布。
首先我想到的是,对各个孩子进行标记。如果小偷偷了某个节点,则该节点的左右孩子就不能偷,因此将两个节点标记为0,表示不可偷。如果没有偷该节点,则将左右孩子标记为1,表示可以偷。然后通过递归解决。
class Solution {
public:
int rob(TreeNode* root)
{
return robber(root,1);
}
int robber(TreeNode* root,int house)
{
if(root==nullptr)
return 0;
if(house==1&&root->left==nullptr&&root->right==nullptr)
return root->val;
if(house==0&&root->left==nullptr&&root->right==nullptr)
return 0;
if(house==0)
return robber(root->right,1)+robber(root->left,1);
else return max(root->val+robber(root->right,0)+robber(root->left,0),robber(root->right,1)+robber(root->left,1));
}
};
但是因为没有采用动态规划,算法性能较低,没想到第一次就通过了测试:
但用了2528ms,可以说很恐怖了。
这次我们换个思路,对于某个节点的最大偷窃财富,其实就可以理解为
4个孙子偷的钱 + 爷爷的钱 VS 两个孩子偷的钱 哪个组合钱多,就当做当前节点能偷的最大钱数。这就是动态规划里面的最优子结构
class Solution {
public:
int rob(TreeNode* root)
{
if(root==nullptr)
return 0;
int money=root->val; //爷爷的钱
if(root->left!=nullptr)
money+=rob(root->left->left)+rob(root->left->right);
if(root->right!=nullptr)
money+=rob(root->right->left)+rob(root->right->right); //四个孙子的钱
return max(money,rob(root->left)+rob(root->right)); //两个孩子的钱和之前总和的最大值
}
};
可以看出效率并没有提升,但是代码简化了,也为后面的方法做了铺垫。
一脉相承,我们依然采用备忘录来解决重复子问题。但是由于这是一个树形结构,而且每次递归只有根节点的指针,而没有相应的数据,很难用数组进行存储,因此我选择采用map进行存储。
对map不熟悉的可以看看 这篇博文,以及这篇
下面直接上代码:
class Solution
{
private:
map<TreeNode*,int> maps;
public:
int rob(TreeNode* root)
{
if(!root)
return 0;
auto it_find=maps.find(root); //在map中寻找有没有储存该节点的值
if(it_find!=maps.end()) //如果it_find没有指向末尾,证明找到了
return it_find->second;
int money=root->val;
if(root->left!=nullptr)
money+=rob(root->left->left)+rob(root->left->right);
if(root->right!=nullptr)
money+=rob(root->right->left)+rob(root->right->right);
maps.emplace(root,max(money,rob(root->left)+rob(root->right))); //向map中插入新的节点
return max(money,rob(root->left)+rob(root->right));
}
};