在牛客网上做的题,对0-1背包问题算法的空间优化方法很纠结,顺了很久终于顺通了思路,记录下来以防忘记。
北大网络实验室经常有活动需要叫外卖,但是每次叫外卖的报销经费的总额最大为C元,有N种菜可以点,经过长时间的点菜,网络实验室对于每种菜i都有一个量化的评价分数(表示这个菜可口程度),为Vi,每种菜的价格为Pi, 问如何选择各种菜,使得在报销额度范围内能使点到的菜的总评价分数最大。 注意:由于需要营养多样化,每种菜只能点一次。
输入的第一行有两个整数C(1 <= C <= 1000)和N(1 <= N <= 100),C代表总共能够报销的额度,N>代表能点菜的数目。接下来的N行每行包括两个在1到100之间(包括1和100)的的整数,分别表示菜的>价格和菜的评价分数。
输出只包括一行,这一行只包含一个整数,表示在报销额度范围内,所点的菜得到的最大评价分数。
1.朴素解法
一开始写出状态方程(d[ i ].cost和d[ i ].val里存着第i件物品的费用和评价值)
写出了下面这段,测试用例能通过,但是提交不了。
#include
#include
#include
using namespace std;
int dp[101][1001]={{0}};//dp[物品][容量]
struct dish{
int cost;
int val;
dish(){}
dish(int a,int b):cost(a),val(b){}
};
vector d;
int main(){
int n,c,cost,val;
while(cin>>c>>n){
d.push_back(dish(0,0));
for(int i=0;i>cost>>val;
d.push_back(dish(cost,val));
}
for(int i=0;i<=n;i++){
dp[0][i]=0;
}
for(int i=0;i<=c;i++){
dp[i][0]=0;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=c;j++){
if(j>=d[i].cost)dp[i][j]=max(dp[i-1][j],dp[i-1][j-d[i].cost]+d[i].val);
else dp[i][j]=dp[i-1][j];
}
}
cout<
2.空间优化法
看了题解才知道是空间复杂度太高了,需要把dp优化成一维数组。按照书上的说法,因为在之前的代码中,dp[ i ][ j ]其实仅和其上一行的dp[ i-1 ][ j ]和dp[ i-1 ][ j-d[i].cost ]两项直接关联,所以可以把dp[ i ][ j ]=max(dp[ i-1 ][ j ],dp[ i-1 ][ j-d[i].cost ]+d[ i ].val); 简化为 dp[ j ]=max(dp[ j ],dp[ j-d[i].cost ]+d[ i ].val); 条件是“此时必须保证在更新dp[ j ]时dp[ j-d[i].cost ]尚未被本次更新修改” ,所以更新dp的循环体改为
for(int i=0;i=d[i].cost;j--){//倒序遍历
dp[j]=max(dp[j],dp[j-d[i].cost]+d[i].val);
}
}
刚看到这里的时候我是懵的,想不懂为什么 j 循环要倒叙,想不懂上面划线的句子是什么意思,直到尝试把每次更新后的dp数组打印出来,恍然大悟。
我输入了一个简单的情况,总容量只有10,菜品有三种,然后依次输入三道菜品各自的费用和评价值
本题本质上就是0-1背包问题,为了更好地描述和理解,下面的主语不用本题的设定,而是用背包和物品,背包容积等价于题目中的报销经费总额,物品等价于题目中的菜品,物品的体积和价值分别等价于题目中的费用和评价值。
第一次更新的是dp[10],此时外层循环中的 i=0,内层循环中的 j=10,即在(只能放第0号物品)&&(最大可容纳体积为10)的情况下,装的物品的最大价值为5。
第二次更新的是dp[9],此时外层循环中的 i=0,内层循环中的 j=9,即在(只能放第0号物品)&&(最大可容纳体积为9)的情况下,装的物品的最大价值为5。
……
在第九次更新完dp[2]后,i=0,j自减1变成了1,又因为0号物品至少要2的容积才能装,所以停止遍历,跳出 j 循环,i+=1;
第十次更新的是dp[10],此时外层循环中的 i=1,内层循环中的 j=10,即在(能放第0号物品和第1号物品)&&(最大可容纳体积为10)的情况下,装的物品的最大价值为13。
……
知道了他大概的更新流程后,来假想一下如果不是逆序更新,那么第一个更新的就会是dp[ 2 ],接着根据转移方程dp[ j ]=max(dp[ j ],dp[ j-d[i].cost ]+d[ i ].val) 进行更新,因为0号物品(菜品)的体积(cost)是2,价值(val)是5,所以当更新dp[ 4 ]时,dp[ 4 ]=max(dp[ j ],dp[ j-d[i].cost ]+d[ i ].val)=max(dp[ 4 ],dp[ 4-2 ]+5)=5+5=10,也就是放了两个0号物品进背包,这就跟我们题目要求的每个物品只能放一次矛盾了,因此,j 必须逆序更新。
至于为什么朴素算法中不用逆序而这里就要呢?我的理解是这样的:
//此处节选两种算法中的dp更新部分
//朴素算法
for(int i=1;i<=n;i++){
for(int j=1;j<=c;j++){
if(j>=d[i].cost)dp[i][j]=max(dp[i-1][j],dp[i-1][j-d[i].cost]+d[i].val);
else dp[i][j]=dp[i-1][j];
}
}
//空间优化
for(int i=0;i=d[i].cost;j--){//倒序遍历
dp[j]=max(dp[j],dp[j-d[i].cost]+d[i].val);
}
}
把for(int i=0;i 在朴素算法中,一轮更新对应dp数组中的一行,不存在后面轮次更新的数据直接覆盖前面更新的数据,dp更新时寻找dp[ i-1 ][ j-d[ i ].cost ]和dp[ i-1 ][ j ],也就是找寻找上一轮更新得到的第 j-d[i].cost个值和第j个值。 在空间优化后,dp数组只有一维,后面轮次更新的数据会直接覆盖前面轮次更新的数据,也就是说,在第i次更新时会把第i-1次更新时得到的数据覆盖掉,然而由朴素算法可知,dp的更新是依赖于上一轮更新的结果的,dp[ j ]更新直接依赖于dp[ j-d[ i ].cost ],dp[ j-d[ i ].cost ]位于dp[ j ]的左边,如果我们还是顺序更新,那么dp[ j-d[ i ].cost ]将早于dp[ j ]被更新,当要更新dp[ j ]时,我们拿到的dp[ j-d[i].cost ]已经不是我们所需要的在上一轮更新中得到的dp[ j-d[i].cost ]。 最后放上空间优化后的代码,并提醒自己一句:背包问题不需要对物品按大小进行排序。 #include