复式学习笔记---0-1背包问题算法空间优化理解(动态规划)

在牛客网上做的题,对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件物品的费用和评价值)

 

  1. j容量能装下物品i:dp[ i ][ j ]=max(dp[ i-1 ][ j ],dp[ i-1 ][ j - d[ i ].cost ] +d[ i ].val)   
  2. j容量不能装下物品i:dp[ i ][ j ]=dp[ i-1 ][ j ]

写出了下面这段,测试用例能通过,但是提交不了。

#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。

……

复式学习笔记---0-1背包问题算法空间优化理解(动态规划)_第1张图片

知道了他大概的更新流程后,来假想一下如果不是逆序更新,那么第一个更新的就会是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
#include
#include
using namespace std;

int dp[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){
        for(int i=0;i>cost>>val;
            d.push_back(dish(cost,val));
        }
        fill(dp,dp+n,0);
        for(int i=0;i=d[i].cost;j--){
                dp[j]=max(dp[j],dp[j-d[i].cost]+d[i].val);
            }
        }
        cout<

 

 

 

 

 

 

你可能感兴趣的:(复试学习笔记,算法,动态规划,数据结构,c++)