在处理到第i个物品时,可以假设一共只有i个物品,如果前面i-1个物品的总的最大价值已经定下来了,那么第i个物品选不选将决定这1~i个物品能带来的总的最大价值
刚刚是自顶向下,接下来反过来自底向上,第1个物品选不选可以轻松地用初始化解决,接下来处理第i个物品时,假设只有2个物品就好,那他处理完后前2个物品能带来的最大总价值就确定了,这样一直推下去,就可以推出前n个物品处理完后能带来的最大总价值
每个物品只能装一次,那么就应该想到常用的一种方法,就是用数组的纵轴来解决,对于n个物品,为它赋予i=1~n的编号,那么数组的纵轴就有n层,每层只考虑装不装这个物品,那么分层考虑就可以解决最多装一个的问题了
对于每个背包,都只有0和1的情况,也就是拿或者不拿两种情况
如果拿:那么空间就会减一点,比如说现在在考虑第i个物品拿不拿,如果说当前剩余空间为j,那么拿了之后空间就变为j-c[i],但是总价值却会增加一点,也就是增加w[i]
如果不拿:那么空间不会变,还是j,但是总价值也不会变化
所以对于这题来说有一个限制条件,就是空间不超出,然后目标就是在空间不超出的情况塞入物品使总价值最大,在前面,我们已经讲了数组的纵轴用来表示当前处理到第几个物品,那么只靠这个是不够的,而且这个数组的意义还没有讲
这题就是限制条件(空间)与价值的平衡,你往背包中塞东西,价值多了,可是空间少了,这空间本来可能遇到性价比更高的物品但也可能没遇到
有了前面的限制情况和0,1的分析就可以建立数组了
对于这个数组,结合题目要求来说,数组的意义肯定是当前的总价值,也就是第i个物品的总价值,那么题目还有一个限制条件,只靠一个n层的一维数组是不够的,还需要二维数组的横轴来分析当前的剩余容量
所以我们有了一个数组可以来解决问题了,这个数组就叫f好了,然后它是一个二维数组,它的纵轴有i层,我希望它从i=1~n,不想从下标0开始是为了美观,然后这个二维数组的横轴代表着当前剩余的空间,就用j来表示,j=0~V,0就是没有空间的意思,V前面说了,是这个背包的总容量
为什么要遍历v~0?
其实这个就是为了遍历放这个 i 物品的所以空间情况
我们把这个二维数组建立在int main()的上面,所以它一开始全部都是0,省去了接下来赋初值为0的功夫
有了数组f[i][j],然后对于每个f[i][j],它表示的是已经处理到第i个物品了,当剩余空间还有j时,能带有的最大价值,也就是说f[i][j]存储的是总价值
说是总价值,可是涉及到放物品还是不放物品的问题,所以再细致点就是:当前剩余空间为j,用这j空间取分析第i个物品装不装如,处理执行完行为后,f[i][j]就表示了当前能装入的最大价值
为什么要用max函数?
有一些偏极端情况,放入这个物品,也许它价值w[i]很高,但是它占用空间c[i]也大,它的性价比可能很低,所以这时候就需要max函数了
当还有空间时:F[i,j] = max[F[i−1,j],F[i−1,j−C[i]] + W[i]
当空间不够时:F[i,j] = F[i−1,j]
下面这个就是01背包的普通代码:
1 #include<bits/stdc++.h>//万能头文件 2 #define ll long long 3 using namespace std; 4 const ll maxn=100; 5 ll n,v,f[maxn][maxn]; 6 ll c[maxn];//每个物品占用空间 7 ll w[maxn];//每个物品的价值 8 int main() 9 { 10 cin>>n>>v; 11 for(ll i=1;i<=n;i++) 12 scanf("%lld",&c[i]); 13 for(ll i=1;i<=n;i++) 14 scanf("%lld",&w[i]); 15 for(ll i=1;i<=n;i++)//第i个物品 16 for(ll j=v;j>=0;j--)//剩余空间j 17 { 18 if(j >= c[i])//如果装得下 19 f[i][j]=max( f[i-1][j-c[i]]+w[i],f[i-1][j]); 20 else//如果装不下 21 f[i][j]=f[i-1][j]; 22 } 23 cout<<f[n][v]<<endl;//输出答案 24 25 }
有了前面基础版01背包的学习,现在学习这个就容易多了
在01背包中通过对数组的优化(用了滚动数组的方法),可以使本来N*V的空间复杂度降低到V,也就是把关于第几个物品的N去掉了(下面会解释为什么可以这么做)
至于为什么要空间优化,首先是因为递推本来就是用空间换时间,消耗的空间比较大,然后关于算法的竞赛一般都会有空间的限制要求,最后,在找工作面试时,面试官肯定会问一些优化的问题,平时养成优化的习惯面试时也有好处
通过观察可以发现对于普通版的01背包递推式,f[i][...]只和f[i-1][...]有关,那么我们可以用一种占用,一种滚动的方法来循环使用数组的空间,所以这个方法叫滚动数组,对于将来肯定用不到的数据,直接滚动覆盖即可,具体的如何滚动会放下面讲
还有就是滚动数组的缺点是牺牲了抹除了大量数据,不是每道题都可以用,但是在这,答案刚好是递推的最后一步,所以直接输出即可,递推完后不需要调用那些已经没了的数据,所以这题可以
下面先画个图理解一下滚动的大致概念
反正就是不断覆盖的过程
下面开始具体化的分析
对于第i层,它只和第i-1层有关,但是对于剩余空间j无法优化,所以现在拿i开刀,把他砍掉,用一个长度为V(总空间)的数组来表示,然后每次相邻的两个i和i-1在上面一直滚动
所以现在建立一个数组f[V],一维数组大小为V
首先建立两个复合for循环
for(i=1~n)
for(j=v~0)
记住这里第二层循环必须是v~0而不是0~v,先记着,后面会解释,
接下来的分析建议配合下面图片学习
然后在循环的过程中,还是老样子,假设我们已经循环到i=2这层了(也就是说i=1已经循环完了),然后对于i=2这一层,我们对j循环,j从v到0
假如现在j=v,我们让f[j]=max(f[j],f[j-c[i]]+w[i])
在没有覆盖之前,所有的f数据都是属于上一层也就是第一层的,我们就当作i-1层数据已经准备好了,然后把max内的拆成两半分析,对于f[j]=f[j]就是不放的情况,那么总价值没有改变,所以对于f[j]=f[j]就是形式上的更新数据,把i-1层的f[j],给了i-1层的f[j]...对于f[j]=f[j-c[i]]+w[i],那个w[i]是肯定要加的不用讨论,然后我们观察一下,对于下标j-c[i]是不是肯定会小于j,那么如果说j从V~0也就是从最大到最小,每次赋值处理都是从前面的格子看看数据参考,并没有修改
再详细点说的话就是对于f[j]=f[j-c[i]]+w[i],f[j-c[i]]是第i层的东西,让j=v~0是为了让f数组每次滚动覆盖时都是覆盖接下来不需要用的位置,比如说处理到第f[8]位时,假如接下来的max判定后面那种方法总价值大,然后假设c[i]=3,这时后就相当与f[8]=f[8-c[i]=5]+w[i],我们这里只是参考了f[5]的数据,并没有改变它,因为说不定计算新一轮f[6]时又要用到旧的f[5]呢,可是我们刷新了f[8]的数字后,再j--,计算f[7],再j--,计算f[6],都不会再用到f[8]这个数据,这是由于f[j-c[i]] 中的减c[i]导致的,反之,假若我们让j=0~v,就可能出现新数据被新数据覆盖的结果,我们是有"底线"的,只允许新数据覆盖旧数据
对于j,如果要处理f[j]=max(f[j],f[j-c[i]]+w[i]),就得当j>=c[i]时处理,因为如果j<c[i],那么j-c[i]为负,下标负的情况没必要考虑,如果考虑了还可能会溢出
其实对于max,还用另一个小东西代替,有没有发现,如果f[j-c[i]]+w[i]>f[j],就选f[j-c[i]]+w[i],如果f[j-c[i]]+w[i]<f[j],那选f[j]和没选一样,所以待会的空间优化版省掉了max函数,少用一种函数
#include<bits/stdc++.h>//万能头文件 #define ll long long using namespace std; const ll maxn=100; ll n,v,f[maxn]; ll c[maxn];//每个物品占用空间 ll w[maxn];//每个物品的价值 int main() { cin>>n>>v; for(ll i=1;i<=n;i++) scanf("%lld",&c[i]); for(ll i=1;i<=n;i++) scanf("%lld",&w[i]); for(ll i=1;i<=n;i++)//第i个物品 for(ll j=v;j>=0;j--)//剩余空间j { if(f[j]<=f[j-c[i]]+w[i] && j-c[i]>=0 )//二维数组变一维数组 f[j]=f[j-c[i]]+w[i];//如果值得改变并且j的空间还装得下就赋新值 } cout<<f[v]<<endl;//输出答案 }
原博客:https://www.cnblogs.com/zyacmer/p/9961710.html
原文:https://www.cnblogs.com/-Ackerman/p/11228249.html