今天是动态规划学习的第三天,主要的学习内容包括:01背包问题二维数组解法和一维数组解法,以及01背包问题的应用。
01背包问题 二维
题目链接:46. 携带研究材料(第六期模拟笔试) (kamacoder.com)
首先我们需要了解什么是01背包问题。有n件物品和容量为w的书包,第i件物品的质量为w[i],价值为val[i],每个物品只能使用一次,求解将哪些物品装入背包可以获得最大的价值。
首先对于dp[i][j],我们定义为0-i件物品在背包容量为j时的最大价值,如下图所示:
对于递推公式,我们有两种情况:当我们不放入物品i时,我们的dp[i][j]应该是等于dp[i-1][j]的;当我们需要放入物品i时,应该减少书包的容量,并增加i的价值,所以dp[i][j]=dp[i-1][j-w[i]]+val[i];我们需要在这两种情况中选择价值较大的情况。
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
对于初始化,我们需要把第一列初始化为0,因为背包容量为0时肯定啥都装不了;我们需要把第一行的j>=w[0]的情况置为val[0],即当背包容量大于等于第一件物品的质量时,就装入第一件物品;装不下第一件物品就将其赋值为0.
对于遍历顺序,既可以先遍历物品,再遍历背包容量;也可以先遍历背包容量,再便利物品。因为我们dp[i][j]的更新取决于左上角的数值,而这两种遍历方式都会优先把左上角进行赋值,所以在求解时都是可行的。
具体代码实现如下所示:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int n;
int bagweight;
scanf("%d%d",&n,&bagweight);
vector<int> weight(n,0);
vector<int> value(n,0);
for(int i=0;i<n;i++) scanf("%d",&weight[i]);
for(int i=0;i<n;i++) scanf("%d",&value[i]);
vector<vector<int>> dp(n,vector<int>(bagweight + 1, 0));
for(int i=weight[0];i<=bagweight;i++)
{
dp[0][i]=value[0];
}
for(int i=1;i<n;i++)
{
for(int j=0;j<=bagweight;j++)
{
if(j<weight[i]) dp[i][j]=dp[i-1][j];
else dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
}
}
printf("%d",dp[n-1][bagweight]);
}
01背包问题 一维
题目链接:46. 携带研究材料(第六期模拟笔试) (kamacoder.com)
对于01背包问题,我们也可以对二维数组进行状态压缩,从而使用一维数组解决01背包问题。
首页是dp[j]的含义,我们定义为在背包容量最大为j时的最大价值。
对于递推公式,还是分成那两种情况,当不装入物品时,dp[j]=dp[j];当装入物品时,dp[j]=dp[j-w[i]]+val[i],取两种情况的较大值即可。
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
对于遍历顺序,这个就有讲究了。
首先我们先来说一下正确的遍历顺序,外层循环应该是物品的顺序,内层循环应该是背包的容量,并且应该倒序遍历,具体代码如下。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
首先,我们先来讲一下为什么内层循环一定要倒序遍历。因为我们其实是将二维数组压缩到一维数组内,假如我们正序遍历,物品就会被多次放入。举个例子,假如w[0]=1,val[0]=15,那么dp[0]=0;
dp[1]=dp[1-w[0]]+val[0]=15,dp[2]=dp[2-w[0]]+15=dp[1]+15=30.在这个例子中,物品0就已经被重复添加了。因为在这个过程中,我们更新所用到的数值已经在前面的更新过程中更新过了。可以理解为我们是将二维数组一行一行的赋值到一维数组内,然而更新时已经用到了前面更新的值,这当然会出现问题。所以我们只能逆序遍历来保证物品只会被使用一次。
接下来我们讨论一下能否将外层循环和内层循环的顺序颠倒,答案也是不行的。假如把顺序颠倒,将背包容量的便利放在外面,就意味着每个包只放入了最大价值的一个物品。这显然也是不符合我们的要求的。所以只能物品在外循环,背包容量在内循环。
具体代码实现如下所示:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int n;
int bagweight;
scanf("%d%d",&n,&bagweight);
vector<int> weight(n+1,0);
vector<int> value(n+1,0);
for(int i=0;i<n;i++) scanf("%d",&weight[i]);
for(int i=0;i<n;i++) scanf("%d",&value[i]);
vector<int> dp(bagweight+5,0);
for(int i=0;i<n;i++)
{
for(int j=bagweight;j>=weight[i];j--)
{
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
}
}
printf("%d",dp[bagweight]);
}
416. 分割等和子集
题目链接:416. 分割等和子集 - 力扣(LeetCode)
我一开始其实是想暴力递归的,然后就超时了。所以使用动态规划进行求解。
我们将其带入背包问题,数组的元素值既是重量又是价值,背包容量是sum/2,物品的遍历是0-n,如果dp[sum/2]==sum/2,就说明是可以满足条件的,否则返回false。具体代码实现如下所示:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum=0;
for(int i=0;i<nums.size();i++) sum+=nums[i];
if(sum%2==1) return false;
int target=sum/2;
vector<int> dp(target+1,0);
for(int i=0;i<nums.size();i++)
{
for(int j=target;j>=nums[i];j--)
{
dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
if(dp[target]==target) return true;
else return false;
}
};