蒟蒻的9个背包的浩大工程(更新中)(无限延期)(太长了不舍删虽然写的lj的一匹)
所以说这就是一篇写炸的废文!!!!
所以说背包直接看dd大神的就好了,算了瞎写写吧。
0/1背包
有n件物品和一个容量为C的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品放入背包可使背包的价值总和最大。
0/1背包是最基础的背包问题,问题的特点是:每种物品只有一件,那么对于第i种物品一共只存在两个状态:取或不取。
定义状态:用f[i][c] 表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。
那么很容易可以得到状态转移方程:
f[i][j]=max{ f[i-1][c] , f[i-1][c-w[i]]+v[i] }
这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。
如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[i-1][v]; 如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。(以上两段话来自dd神犇博客)
据dd神犇所讲,对于上述方程,时间复杂度和空间复杂度为O(N*V),时间复杂度已经无法优化,而空间复杂度可以优化;具体操作详见dd大神博客。
① 百度—搜索背包九讲—找一个写的最强大的CSDN博客
② 传送门:http://blog.csdn.net/ling_du/article/details/41594767(搬运)
回到正题,虽然我是讲不清楚(早晚弄懂回来讲一遍)代码还是要放的
for(int i=1;i<=n;i++){
for(int c=C;c>=w[i];c--)
f[c]=max(f[c],f[c-w[i]]+v[i]);
}
需要注意的是,第二层循环的顺序十分重要,如果写成顺推,就会出现已经更新过的状态被重复更新,实际上这可以用来解决完全背包,那么如何才能让每个状态只被更新一次?逆推。我们想一下,我们所写的方程,利用的是c-w[i],如果正着推,不可避免的会出现c-w[i]=之前被更新过的某个状态,但倒着显然就不会,因为c-w[i]是越变越小的,这样就达到了每个物品只取一次的目的。当然…这是我yy+显然的证明法,具体解释还是去dd神犇博客慢慢看吧。
完全背包
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
对于完全背包,它和0/1背包的区别就在于0/1背包的每件物品只能用一次,而完全背包每件物品可以使用无限次。基本解法就是将它转变成0/1背包来解,相较于0/1背包,似乎要多枚举一层件数?
状态转移方程:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}
这跟01背包问题一样有O(N*V)个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态f[i][v]的时间是O(v/c[i]),总的复杂度是超过O(VN)的。
将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是试图改进这个复杂度。(来自dd神犇)。
还记得0/1背包中被我yy+显然得出的结论吧…..
没错!0/1背包简化成一维后的第二重循环我们只要把逆推改为正推就行!既然完全背包需要重复计算,那我们就用曾经被我们否决掉的能用来重复计算的方案就好。(更严谨的解释…你们知道我要让你们看什么)。
代码:
for(int i=1;i<=n;i++){
for(int c=w[i];c<=C;c++)
if[c]=max( f[c] , f[c-w[i]] + v[i]);
}
/*来自dd神犇博客:完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。
这个优化可以简单的O(N^2)地实现,一般都可以承受。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)地完成这个优化。*/
多重背包
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
和完全背包类似的问题,除了每件物品有固定件数,循环改一下即可,方程:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}
没错就只是加了一个数量限制而已。
然后我们想着试图优化这个方程。
转化为01背包问题
另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成n[i]件01背包中的物品,则得到了物品数为Σn[i]的01背包问题,直接求解,复杂度仍然是O(V*Σn[i])。
但是我们期望将它转化为01背包问题之后能够像完全背包一样降低复杂度。仍然考虑二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问题中第i种物品可取的每种策略——取0..n[i]件——均能等价于取若干件代换以后的物品。另外,取超过n[i]件的策略必不能出现。
方法是:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为1,2,4,...,2^(k-1),n[i]-2^k+1,且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。
分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分0..2^k-1和2^k..n[i]两段来分别讨论得出,例如:
13=2^0+2^1+2^2+6
对于件数足够多的,我们可以将它视为完全背包来写。代码:
for(int i=1;i<n;i++){
if(w[i]*a[i]>C){
for(int c=0;c<=C;c++)
if(c>=w[i])
f[c]=max(f[c],f[c-w[i]]+v[i]);
}
else{
int k=1,amount=a[i];
while(k<amount){//想一想刚才说的二进制法
for(int c=C;c>=k*w[i];c--)
f[c]=max(f[c],f[c-k*w[i]]+k*v[i]);
amount-=k;
k+=k;
}
for(int c=C;c>=amount*w[i];c--)//把剩下的当成一个物品
f[c]=max(f[c],f[c-amount*w[i]]+amount*v[i]);
}
}
混合背包
简而言之,将以上三种背包混在一起,判断一下是哪个背包即可
for(int i=1;i<=n;i++){
if(num[i]==1){//0/1
for(int c=v1;c>=w[i];c--)
f[c]=max(f[c],f[c-w[i]]+v[i]);
}
else if(num[i]*w[i]>v1){//完全
for(int c=w[i];c<=v1;c++)
f[c]=max(f[c],f[c-w[i]]+v[i]);
}
else {//多重
int k=1,amount=num[i];
while(k<amount){
for(int c=v1;c>=k*w[i];c--)
f[c]=max(f[c],f[c-k*w[i]]+k*v[i]);
amount-=k;
k<<=1;
}
for(int c=v1;c>=amount*w[i];c--)
f[c]=max(f[c],f[c-amount*w[i]]+amount*v[i]);
}
}