1 多重背包问题
多重背包问题也是背包问题的基础之一,可以拆分为01背包问题。
多重背包问题:
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
多重背包问题,完全可以把Mi件物体拆分为Mi个物体,从而转化为01背包问题,其他都一样,体现在代码上则是新建一个01背包数组或在01背包里多加一层Mi的循环。
代码如下:
void test_multi_pack(vector<int>& weight, vector<int>& value,vector<int>& nums, int bagWeight){
vector<int> dp(bagWeight + 1, 0);
// 遍历物品
for(int i = 0; i < weight.size(); i++)
{
// 遍历背包容量
for(int j = bagWeight; j >= weight[i]; j--)
{
// 以上为01背包,然后加一个遍历个数
// 遍历个数
for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++)
{
dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
}
}
return dp[bagWeight];
}
2 打家劫舍
LeetCode:打家劫舍
有两种思路,推荐使用第一种思路进行思考:
1 dp[i]代表从0-i个房间内所能获得的最大金额,对于房间i而言,有偷与不偷两种情况,偷的情况dp[i]=dp[i-2]+nums[i];不偷的情况dp[i]=dp[i-1],因此递推公式为dp[i]=max( dp[i-1] , nums[i]+dp[i-2] )
2 dp[i]的含义是必须偷了i的情况下,从0-i的最佳金额,dp数组不一定会保持单调递增,但是dp[i+2]>=dp[i]必然成立,所以递推公式为dp[i]=max(dp[i-2],dp[i-3])+nums[i],最终结果在dp[n-1]和dp[n-2]中选
思路一(推荐)
class Solution {
public:
int rob(vector<int>& nums) {
//长度<=1的情况
if(nums.size()==1)
return nums[0];
//dp[i]前i个房间可以偷到的最多金额
//显然,dp[i]分为两种情况,偷i和不偷i,取最大值
//偷i的情况下,dp[i]=dp[i-2]+nums[i]
//不偷i的情况下,dp[i]=dp[i-1]
int n=nums.size();
vector<int> dp(n,0);
dp[0]=nums[0];
dp[1]=max(nums[0],nums[1]);
for(int i=2;i<n;++i)
{
dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[n-1];
}
};
思路二
class Solution {
public:
int rob(vector<int>& nums) {
//dp[i]代表从0-i且必须偷了i的情况下的最佳金额
//dp[i]=max(dp[i-2],dp[i-3])+nums[i]
//最终最大值为dp[n]和dp[n-1]中的最大值
//长度<=2的情况
if(nums.size()==1)
return nums[0];
if(nums.size()==2)
return max(nums[0],nums[1]);
//长度>=3
int n=nums.size();
vector<int> dp(n,0);
dp[0]=nums[0];
dp[1]=nums[1];
dp[2]=nums[0]+nums[2];
for(int i=3;i<n;++i)
{
dp[i]=max(dp[i-2],dp[i-3])+nums[i];
}
return max(dp[n-1],dp[n-2]);
}
};
3 打家劫舍II
LeetCode:打家劫舍II
考虑环就无非三种情况:
- 1 考虑首,不考虑尾[2~n-1]
- 2 考虑首,不考虑尾[1~n-1]
- 3 考虑尾,不考虑首[2~n]
事实上,因为3已经包括情况1,我们只需要考虑[1 ~ n - 1]和[2 ~ n]中的最大值即可。
class Solution {
public:
int robAssist(vector<int>& nums,int start,int end)
{
vector<int> dp(end-start+1,0);
dp[0]=nums[start];
dp[1]=max(nums[start+1],dp[0]);
for(int i=2;i<dp.size();++i)
{
dp[i]=max(dp[i-1],dp[i-2]+nums[start+i]);
}
return dp.back();
}
int rob(vector<int>& nums) {
//分为三种情况
//1 不考虑头也不考虑尾
//2 考虑头,不考虑尾
//3 考虑尾,不考虑头(包含了第1种情况,因为不一定选了尾)
//实际上只需要考虑2~n和1~n-1两种情况
if(nums.size()==1)
return nums[0];
if(nums.size()==2)
return max(nums[0],nums[1]);
int result1=robAssist(nums,0,nums.size()-2);
int result2=robAssist(nums,1,nums.size()-1);
return max(result1,result2);
}
};
4 打家劫舍III
LeetCode:打家劫舍III
后序遍历天然具有从小规模相似问题->目标问题的转化状态,因此必然可以采用这种方法进行动态规划,推荐使用方法一。
- 方法一采用了动态规划,其dp状态数组代表着目前节点上选目前节点的最大值和不选目前节点的最大值,即dp={ 不选 , 选 },因此向根部状态时,dp[0] = max( left[0] , left[1] ) + max( right[0] , right[1] ),而dp[1] = left[0] + right[0] + root->val。
- 方法二采用了记忆化递归,将中间结果都保存在树的节点上,需要用时进行实时的查询。
方法一:动态规划
class Solution {
public:
//树形动态规划,每一个节点都计算{当前不偷最大值,当前偷最大值}
vector<int> traversal(TreeNode* root)
{
//递归终止条件,也是初始化参数
if(root==nullptr)
return vector<int>{0,0};
vector<int> left=traversal(root->left);
vector<int> right=traversal(root->right);
//当前不偷的情况下,直接用左右节点的最大值
int no_steal=max(left[0],left[1])+max(right[0],right[1]);
//当前偷的情况下,只能用左右不偷的值+当前值
int steal=left[0]+right[0]+root->val;
return vector<int>{no_steal,steal};
}
int rob(TreeNode* root) {
vector<int> dp=traversal(root);
return max(dp[0],dp[1]);
}
};
方法二:记忆化递归
class Solution {
public:
int getSonValue(TreeNode* root)
{
int sum=0;
if(root->left!=nullptr)
sum+=root->left->val;
if(root->right!=nullptr)
sum+=root->right->val;
return sum;
}
void traversal(TreeNode* root)
{
if(root==nullptr || (root->left==nullptr && root->right==nullptr))
return;
//后序遍历
//左
traversal(root->left);
//右
traversal(root->right);
//中
if(root->left!=nullptr && root->right==nullptr)
root->val = max(root->left->val,
getSonValue(root->left) + root->val);
else if(root->left==nullptr && root->right!=nullptr)
root->val = max(root->right->val,
getSonValue(root->right) + root->val);
else
root->val = max(root->right->val + root->left->val,
getSonValue(root->left) + getSonValue(root->right) + root->val);
}
int rob(TreeNode* root) {
traversal(root);
return root->val;
}
};
5 总结
打家劫舍系列还是感觉比较简单的,基本上围绕着dp数组的定义和转化进行,初始化反而是符合逻辑的。
今天不仅把昨天做的完全背包更新了,还做了点打家劫舍,明天再开始股票系列吧。
——2023.3.4