(我在做POJ 1276 的时候讨论区都在感谢背包九讲,虽然到最后AC我都没有用到,不过我下载下来准备学习一番。因为我觉得背包我好像懂了但其实没懂,好像没懂吧但又真的没懂 = =。。。0-1背包只会用贪心。。其他背包状态转移方程会列的不会写,不会列的更不会写(为什么写这么多废话 = =)。本文基本上是背包九讲里的原文各种节选和化用,而【注】里则是我个人的一些理解。)
【第一讲】 0-1背包
1、简述题意 有 n 个不同的质量为 Wi ( i = 0....n),体积为 Ci( i = 0....n)的物体装入容积为 V 的背包中
2、状态转移方程: F[i, v] = max{ F[i - 1, v], F[i - 1, v - Ci] + Wi },F[i, v] 含义是将前 i 件物品放入体积为 v 的背包中的最大重量和,若只考虑第 i 件物品的放置情况,则转化为了为一个只和前i - 1件物品相关的问题。
伪代码 1
F[ 0, 0...V] = 0;
for i = 1 to n;
for j = V to Wi
F[ i][ j] = max{ F[ i-1][ v], F[ i-1][ j-Ci] + Wi }
【注 :当然,伪代码2、3也都是可行的
伪代码 2
F[ 0, 0...V] = 0;
for i = 1; i <= n
for j = Ci to v // 这个时候要进行一步赋值
{ F[ i][ j] = ( i == 1 ? 0 : F[ i - 1][ j])
F[ i][ j] = max{ F[ i-1][ v], F[ i-1][ j-Ci] + Wi }
}
伪代码 3 (F[i, v] = max{ F[i + 1, v], F[i + 1, v - Ci] + Wi } 表示第 i 层物品放入体积为 v 的背包中时的最大重量和)
F[ 0, 0...V] = 0;
for i = n to 0 // 此时 i 必须逆序枚举,j 的枚举顺序无关紧要
for j = Ci to v
{ F[ i][ j] = ( i == 1 ? 0 : F[ i - 1][ j])
F[ i][ j] = max{ F[ i-1][ v], F[ i-1][ j-Ci] + Wi }
} 】
3、优化空间复杂度可以将二维数组转换成一维数组(滚动数组的形式)
“肯定是有一个主循环i = 1...N,每次算出来二维数组F[i; 0....V ]的所有值。" 但是对于每一个状态F[i, v]而言,或许 i 值不是必要的,只需要保证“ 第i次循环结束后F[v]中表示的就是我们定义的状态F[i, v] ”。
“ F[i; v]是由F[i -1; v]和F[i - 1; v - Ci]两个子问题递推而来,也就是需要保证在推F[i; v]时(也即在第i次主循环中推F[v]时)能够取用F[i - 1; v]和F[i - 1; v - Ci]的值 ”,事实上这要求以v = V....0的递减顺序计算F[v]。
【注:刘汝佳的书里也有很好的解释:因为 F 数组是从上到下计算的,所以F中原先存储的一定是 i - 1次循环的值(这毋庸置疑),此时F[ v ]里保存的是F[i - 1; v]的值,F[ v - Ci]里也保存的是F[i - 1; v - Ci]的值(这也毋庸置疑)。所以在下次循环中,要用max{ F[i - 1, v], F[i - 1, v - Ci] + Wi } 的值覆盖掉F[ v ],那么在计算F[ v ]时必须有上一次F[ v - Ci]的值,所以应该按照递减顺序计算】
伪代码4
F[0...V ] = 0
for i = 1 to N
for v = V to Ci
F[v] = max[ F[v], F[v - Ci] +Wi}
作者定义一维数组解0-1背包的解法简化为这样一个过程def ZeroOnePack(F;C;W),有了这个过程以后,01背包问题的伪代码这样写:
伪代码4
for i = 1 to N
ZeroOnePack(F;Ci;Wi)
4、初始化问题
当题目为 恰好装满背包时,应在初始化时除了F[0] = 0,其他F[ 1...V] 都为负无穷,保证为恰好装满背包的最优解
【注:F[0]为0,保证了所有的值都是由0加得的,而不是进行某一步莫名其妙的赋值得来的】
没有要求装满,只要求价格尽量大,全部初始化为零就可以了
“ 初始化的F数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装且价值为0的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。”
【第二讲】 完全背包
1、简述题意 有N种物品和一个容量为V 的背包,每种物品都有无限件可用。放入第i种物品的耗费的空间是Ci,得到的价值是Wi。求最大价值。
2、状态转移方程:这里和0-1背包的区别在于无限个数的物品,物品的取用也是遵循着取或不取两种状态,则状态转移方程为:F[i, v] = max{ F[i - 1, v - k*Vi] + k*Wi | 0< k*Ci < v}
3、简单而有效的优化:“若两件物品i、j满足Ci <= Cj且Wi >= Wj,则将可以将物品j直接去掉,不用考虑。比较不错的一种方法是:首先将费用大于V 的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V + N)地完成这个优化”。
4、转化为0-1背包问题求解
第i种物品最多选 V/Ci 件,于是可以把第i种物品转化为 V/Ci 件费用及价值均不变的物品,然后求解这个0-1背包问题。更高效的转化方法是:把第i种物品拆成费用为Ci*2的k次方、价值为Wi*2的k次方的若干件物品,其中k取遍满足 Ci*2的k次方 <= V 的非负整数。
这是二进制的思想。因为,不管最优策略选几件第i种物品,其件数写成二进制后,总可以表示成若干个 2的k次方 件物品的和。这样一来就把每种物品拆成O(log V/Ci)件物品,是一个很大的改进。
【注:这样的简化缩小k的范围,这里伪代码的话需要加一个对 k 的循环,也就是三重循环。和多重背包也就是数据范围上的不同,伪代码写在多重背包的基本算法处了】
5、算法实现
伪代码5
for i = 1 to n
for j = Ci to V
F[v] = max(F[v]; F[v - Ci] +Wi)
其实只有 j 的循环顺序的差别。
这个算法的可行性在于:0-1背包时 v 递减是为了保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果F[i -1; v - Ci]。。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“ 加选一件第i种物品 ”这种策略时,却正需要一个可能已选入第 i 种物品的子结果F[i; v - Ci],所以就可以并且必须采用v递增的顺序循环。
【注:也就是说,每一次 j 的循环都表示,考虑 j 是否加选第 i 种物品。之前的逆序循环是为了防止一个 j 中有两个 i 物品,所以在 j 的循环前进过程中,j 永远是不包含 i 物品的;而如果顺序循环,则 j 有可能包含 i 物品,这正是我们所需要的】
伪代码中两个for可以颠倒顺序,状态转移方程为F[i; v] = max(F[i - 1; v]; F[i; v - Ci] +Wi)
【注:这很好理解,这种同样是对每一个数值遍历每一个可能加选的 i 种物品。】
最后抽象出处理一件完全背包类物品的过程伪代码:def CompletePack(F;C;W)
for v = C to V
F[v] = maxfF[v], f[v - C] +W}
6、小结
完全背包问题的两个状态转移方程:
F[i, v] = max{ F[i - 1, v - k*Vi] + k*Wi | 0< k*Ci < v} 【注:前i种物品恰放入一个容量为v的背包的最大权值】
F[i; v] = max(F[i - 1; v]; F[i; v - Ci] +Wi) 【注:前i种物品恰放入一个容量为v的背包,对是否加放一个 i 种物品进行决策】
【第三讲】 多重背包问题
1、简述题意 有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci,价值是Wi。求最大价值
2、状态转移方程:和完全背包很相似 F[i,v] = max{ F[i - 1; v - k * Ci] + k* Wi | 0 <= k <= Mi } 这是基本算法
【注:基本算法的伪代码是:
伪代码6
for i = 1 to n
for j = N to Wi
for k = 1 to Mi
F[v] = max{ F[ v - k *Ci] + k*Wi }
】
3、转化为0-1背包问题
我们考虑把第i种物品换成若干件物品,使得原问题中第i种物品可取的每种策略——取0.....Mi件——均能等价于取若干件代换以后的物品。另外,取超过Mi件的策略必不能出现。
方法是:将第i种物品分成若干件01背包中的物品,其中每件物品有一个系数。这件物品的费用和价值均是原来的费用和价值乘以这个系数。令这些系数分别为1; 2; 2^2 .... 2^k-1;Mi - 2^(k+1),且k是满足Mi - 2^(k+1) > 0的最大整数。
例如,如果Mi为13,则相应的k = 3,这种最多取13件的物品应被分成系数分别为1; 2; 4; 6的四件物品。分成的这几件物品的系数和为Mi,表明不可能取多于Mi件的第i种物品。另外这种方法也能保证对于0 ....Mi间的每一个整数,均可以用若干个系数的和表示。
下面给出O(logM)时间处理一件多重背包中物品的过程:
伪代码7
def MultiplePack(F,C,W,M)
if C * M >= V
CompletePack(F,C,W)
return
k = 1
while k < M
ZeroOnePack(kC,kW)
M = M - k
k = 2k
ZeroOnePack(C * M,W * M)
【注:我在网上找了一个程序代码片段,并进行了简要的分析】
- int cash , n ; // cash表示背包总容量
- void ZeroOnePack( int cost ) // 0-1背包过程
- {
- int i ;
- for ( i = cash ; i >= cost ; i-- )
- {
- if ( dp[i] < dp[i-cost]+cost )
- dp[i] = dp[i-cost]+cost ;
- }
- }
- void CompletePack( int cost ) // 完全背包过程
- {
- int i ;
- for ( i = cost ; i <= cash ; i++ )
- {
- if ( dp[i] < dp[i-cost]+cost )
- dp[i] = dp[i-cost]+cost ;
- }
- }
- void MultiplePack( int cost , int count )
- {
- if ( cost*count >= cash ) // 若全取此类物品所占体积大于总容量,转化为完全背包
- {
- CompletePack( cost ) ;
- }
- else // 若比总容量小,则转化为二进制表示的0-1背包
- {
- int k = 1 ;
- while ( k < count ) // 所取的总应小于剩余的
- {
- ZeroOnePack( cost*k ) ; // 考虑加权的商品
- count -= k ;
- k *= 2 ;
- }
- ZeroOnePack( cost*count ) ; // 考虑是否全取此类物品 考虑系数Mi - 2^(k+1)
- }
- }
- int main()
- {
- //前略
- for ( i = 1 ; i <= n ; i++ )
- {
- MultiplePack( node[i].d , node[i].n ) ; // 对每一个物体进行依次决策
- }
- return 0 ;
- }
【第四讲】混合三种背包问题
1、简述题意 将前面1、2、3中的三种背包问题混合起来。也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)
2、01背包与完全背包的混合 也就是说有些物品可以取一次,有些物品可以取无限次。由于0-1背包和完全背包的代码只有一处不同,做一个条件判断就可以了
伪代码8
for i = 1 to N
if 物品 i 属于完全背包
CompletePack( W )
else if 物品 i 属于0-1背包
ZeroOnePack( W )
【注:进一步来说也就是:
for i = 1 to N
if 物品 i 属于完全背包
for j = Ci to S
F[ v ] = max{ F[v], F[v-Ci] + Wi }
else if 物品 i 属于0-1背包
for j = S to Ci
F[ v ] = max{ F[v], F[v-Ci] + Wi }
】
3、再加上多重背包
伪代码9
for i = 1 to N
if 第i件物品属于01背包
ZeroOnePack(F,Ci,Wi)
else if 第i件物品属于完全背包
CompletePack(F,Ci,Wi)
else if 第i件物品属于多重背包
MultiplePack(F,Ci,Wi,Ni)
【第五讲】二维费用背包
1、简述题意 对于每件物品,具有两种不同的空间耗费,选择这件物品必须同时付出这两种代价。对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价一和代价二,第i件物品所需的两种代价分别为Ci和Di。两种代价可付出的最大值(两种背包容量)分别为V 和U。物品的价值为Wi。
2、状态转移方程
状态增加一维 用F[ i, v, u] 表示前 i 个物体在两种代价分别为 v, u时价值的最大值,可以写出状态转移方程F[ i, v, u] = max{ F[ i-1, v, u], F[ i-1, v - Ci, u - Di] + Wi }。
就如前面所述,可以只使用二维数组:当每件物品只可以取一次时变量v和u采用逆序的循环,当物品有如完全背包问题时采用顺序的循环,当物品有如多重背包问题时拆分物品。
【注:伪代码10(0-1背包为例)
for i = 1 to N
for j = V to Ci
for k = U to Di
F[j][k]=max(F[j][k], F[j-v[i]][k-u[i]]+w[i]);
】
3、物品总个数的限制
有时题意中“最多只能取U件物品”,其实就是隐含的二维条件:每多取一件物品就消耗了一次“件数”费用。所以转化成了:每一个物品的件数费用都为1,可以付出最大件数的费用为U的问题。换句话说,设F[v; u]表示付出费用v、最多选u件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在f[0…V; 0...U]范围内寻找答案
4、复整数域上的背包问题
另一种看待二维背包问题的思路是:将它看待成复整数域上的背包问题。也就是说,背包的容量以及每件物品的费用都是一个复整数
5、小结
在原有熟悉的动态规划问题上变形成为新的题目时,往往可以在原来的状态中增加一维以满足新的限制
【第六讲】分组的背包问题
1、简述题意 有N件物品和一个容量为V 的背包。第i件物品的费用是Ci,价值是Wi。这些物品被划分为K组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
2、状态转移方程 问题转化成了很多种状态:选择 k 组中的某一个元素还是整个 k 组都不选。可以列出转态转移方程
F[k; v] = max{ F[k - 1; v], F[k - 1; v - Ci] +Wi | item i ∈ group k }
伪代码11
for k = 1 to K
for v = V to 0
for item i in group k
F[v] = max{ F[v]; F[v - Ci] +Wi }
【注:三重循环保证了每组至多只有一个物品被选中,也可以对组内物品进行优化】
【第七讲】有依赖的背包问题
1、简述题意 这种背包问题的物品间存在某种“依赖”的关系。也就是说,物品i依赖于物品j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。
2、状态转移方程
将不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附件”。由这个问题的简化条件可知所有的物品由若干主件和依赖于每个主件的一个附件集合组成。而按照原来的思路则会将问题处理得非常麻烦:考虑是否包含主件,包含主件后是否包含1个附件,是否包含两个附件.....状态就太多了。
第一步优化:事实上这些状态是互斥的。将一个主件以及它的所有附件集合看成一个物品组,每种 选择了主件又选择了附件 的决策对应于物品组的一个物品,每个物品的价值和费用都是显而易见可以算出来的。
第二步优化:对于第k组的物品,保留最大价值的物品不会影响结果,所以只需要保留最优的一个决策即可
所以,可以对主件k的“附件集合”先进行一次01背包,得到费用依次为0…V -Ck 的所有值时相应的最大价值Fk[0… V - Ck]。那么,这个主件及它的附件集合相当于V - Ck + 1个物品的物品组,其中费用为v的物品的价值为Fk[v - Ck] +Wk,v的取值范围是Ck <= v <= V 。
【注:把每一个主件和它的附件看作一类物品。在取某件物品时,我们只需要从以下四种方案中取最大的那种方案:只取主件、取主件+附件1、取主件+附件2、既主件+附件1+附件2。可以列出状态转移方程:
f[i,j]=max{f[i-1,j],(不取此组)
f[i-1,j-a[i,0]]+a[i,0]*b[i,0],(只取主件)
f[i-1,j-a[i,0]-a[i,1]]+a[i,0]*b[i,0]+a[i,1]*b[i,1],(取主件+附件1)
f[i-1,j-a[i,0]-a[i,2]]+a[i,0]*b[i,0]+a[i,2]*b[i,2],(取主件+附件2)
f[i-1,j-a[i,0]-a[i,1]-a[i,2]]+a[i,0]*b[i,0]+a[i,1]*b[i,1]+a[i,2]*b[i,2]}(主件+附件1+附件2)
在网上找到的代码为(很容易理解)
- for(i=1;i<=m;i++)
- for(j=0;j<=n;j++)
- {
- d[i][j]=d[i-1][j];
- if(j>=w[i][0]) {t=d[i-1][j-w[i][0]]+v[i][0];if(t>d[i][j]) d[i][j]=t;}
- if(j>=w[i][0]+w[i][1]) {t=d[i-1][j-w[i][0]-w[i][1]]+v[i][0]+v[i][1];if(t>d[i][j]) d[i][j]=t;}
- if(j>=w[i][0]+w[i][2]) {t=d[i-1][j-w[i][0]-w[i][2]]+v[i][0]+v[i][2];if(t>d[i][j]) d[i][j]=t;}
- if(j>=w[i][0]+w[i][1]+w[i][2]) {t=d[i-1][j-w[i][0]-w[i][1]-w[i][2]]+v[i][0]+v[i][1]+v[i][2];if(t>d[i][j]) d[i][j]=t;}
- }
】
【第八讲】泛化物品
1、简述题意:在背包容量为V 的背包问题中,泛化物品是一个定义域为0… V 中的整数的函数h,当分配给它的费用为v时,能得到的价值就是h(v)。
【注:这里超出了我的理解范围_(:зゝ∠)_】
【第九讲】背包问题问法的变化
1、常见问题:求定容背包的能装物品的最大价值
2、输出方案
如果要求输出这个最优值的方案,可以参照一般动态规划问题输出方案的方法:记录下每个状态的最优值是由状态转移方程的哪一项推出来的,换句话说,记录下它是由哪一个策略推出来的。便可根据这条策略找到上一个状态,从上一个状态接着向前推即可。
以0-1背包为例,方程为 F[ i, v] = max{ F[i-1, v], F[i-1, v - Ci] + Wi},再用一个数组G[i, v],当使用方程前一项(未选i)为0,后一项(选i)为1,则输出方案的伪代码为
伪代码12【注:最优路径,逆向求解】
i = N
v = V
while i > 0
if G[i; v] = 0
print 未选第i项物品
else if G[i; v] = 1
print 选了第i项物品
v := v - Ci
i := i - 1
当然也可以直接判断 F[ i, v] 等于前一一项还是后一项即可
3、输出字典序最小的最优方案
【注:这里刘汝佳书上提出了一个很好的办法,就是将物品号逆序循环,也就是自底向上求出解答树,就可以很好地求出字典序排列了】
也许更简易的方法是,先把物品编号做x := N + 1 - x的变换,在输出方案时再变换回来。在做完物品编号的变换后,可以按照前面经典的转移方程来求值。只是在输出方案时要注意,如果F[i; v] = F[i - 1; v]和F[i; v] =F[i - 1][v - Ci] +Wi都成立【注:字典序在前的优先】,应该按照后者来输出方案,即选择了物品i,输出其原来的编号N - 1 - i。
4、求方案总数
例如若每件物品均是完全背包中的物品,转移方程即为 F[i; v] = sum{ F[i - 1; v], F[i; v - Ci] },初始条件是F[0; 0] = 1。
事实上,这样做可行的原因在于状态转移方程已经考察了所有可能的背包组成方案。
5、最优方案的总数
F[i; v]代表该状态的最大价值,G[i; v]表示这个子问题的最优方案的总数,则在求F[i; v]的同时求G[i; v]的伪代码如下:
伪代码13
G[0; 0] = 1
for i = 1 to N
for v = 0 to V
F[i; v] := maxfF[i - 1; v]; F[i - 1; v - Ci] +Wig
G[i; v] := 0
if F[i; v] = F[i - 1; v]
G[i; v]+ = G[i - 1][v] 【注:做一次判断,G记录方案总数】
if F[i; v] = F[i - 1; v - Ci] +Wi
G[i; v]+ = G[i - 1][v - Ci]
4、第N优解