背包问题
阅读崔添翼老师《背包九讲》后,个人所作总结
目录
01背包
-
题目描述:有N件物品和一个容量为V的背包。放入第i件物品耗费的费用是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大。
-
特点:每种物品只有一件,选择放或不放。
-
状态:F[i,v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。
-
状态转移方程(在考虑第i件物品时,只有放和不放两种情况):F[i,v]=max{F[i−1,v],F[i−1,v−Ci]+Wi}。
// 伪代码 F[0,0..V]←0 for i ← 1 to N for v ← Ci to V F[i,v] ← max{F[i−1,v],F[i−1,v−Ci]+Wi}
-
边界:相当于啥都不放,背包获得的最大价值(可推广到其它类型的背包问题)
- 要求恰好装满背包:
F[0]为0,其它F[1..V]均设为−∞
- 背包可以不装满:
F[0..V]均设为0
- 要求恰好装满背包:
-
空间优化:用
F[v]
以及第i
次循环表示前i件物品恰放入容量为v的背包获得的最大价值。// 伪代码 F[0..V]←0 for i ← 1 to N // 逆序保证F[v-Ci]是F[i-1,v-Ci],确定物品不会重复取,否则会出现重复取的 for v ← V to Ci F[v] ← max{F[v],F[v−Ci]+Wi} // 综合一下 def ZeroOnePack(F,Ci,Wi) for v ← V to C F[v] ← max(F[v],F[v−C]+W) for i ← 1 to N ZeroOnePack(F,Ci,Wi);
完全背包
-
题目描述:和01背包类似,只不过物品可以是无限件。
-
特点:物品可以是无限件,取0,1,2…⌊V/Ci⌋件。
-
状态:F[i,v]表示前i种物品恰放入一个容量为v的背包可以获得的最大价值。
-
状态转移方程:以01背包的思路,在考虑第i件物品时,有取0,1,2…⌊V/Ci⌋件,从而得到
F[i,v]=max{ F[i−1,v−kCi] + kWi | 0 ≤ kCi ≤ v}
。 -
时间优化
- 对数据的初步筛选O(N2):
若两件物品i、j满足Ci≤Cj且Wi≥Wj
,则将可以将物品j直接去掉,不用考虑。 - (未改进时间复杂度)将无法确定下来的第i种物品,分解成⌊V/Ci⌋件价值相同,花费相同的物品,转换成01背包。
- 第二种的改进版(二进制思想)将第i种物品拆成费用为Ci2k、价值为Wi2k的物件,k取遍Ci2k≤V的所有整数。
- 对数据的初步筛选O(N2):
-
最终O(VN)的算法:
// 伪代码 F[0..V]←0 for i ← 1 to N // 和01背包相反,需要一个可能已选入第i种物品的子结果F[i,v−Ci] for v ← Ci to V F[v] ← max{F[v],F[v−Ci]+Wi} // 综合一下 def CompletePack(F,Ci,Wi) for v ← C to V F[v] ← max(F[v],F[v−C]+W) for i ← 1 to N CompletePack(F,Ci,Wi);
多重背包
-
题目描述:较完全背包,第i种物品最多有Mi件可用。
-
状态:F[i,v]表示前i种物品恰放入一个容量为v的背包可以获得的最大价值。
-
状态转移方程:和完全背包类似,只是限制条件由⌊V/Ci⌋转换为Mi。
F[i,v]=max{F[i−1,v−k∗Ci]+k∗Wi|0≤k≤Mi}
-
时间优化:
- 同样考虑拆分物品转换为01背包,运用二进制的思想,则将物品转换为1,2,4…2(k−1),Mi−2k+1(k是满足Mi−2k+1>0的最大整数),则可以用这些系数组合成0-M的任何数字。将第i种物品分成了O(logMi)种物品。伪代码如下:
def MultiplePack(F,C,W,M) // M的限制太小,转换为完全背包 if C·M > V CompletePack(F,C,W) return // 遍历拆分的所有第i种物品,1,2,4...2^k k ← 1 while k < M ZeroOnePack(F,kC,kW) M ← M - k k ← 2k // 最后一项 ZeroOnePack(F,CM,WM) // 循环每种物品 for i ← 1 to N MultiplePack(F,Ci,Wi,Mi);
-
延申:当考虑
每种若干件的物品能否填满给定容量的背包
时,即不考虑价值,只考虑可行性,可优化为O(VN)
混合背包
-
题目描述:存在只有1件、无限件、M件的物品。
-
直接的想法:判断该种物品为那种类型,调用对应的方法即可。
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)
二维费用背包
-
题目描述:每一个物品,有两种费用,对应的也有两个背包容量,求最大价值。
-
基本思路:在原有基础上,多加一维即可。
-
状态:
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}
-
空间优化:同理,使用F[V,U]。
-
处理不同类型的物品和一维的一致。
-
一维到二维的变形:
- 第二维费用可能是隐含给出,比如物品总件数的限制,注意抽象。
- 若题目由熟悉的DP变形而来,先考虑能否运用之前的状态和转移方程,做一定改变,比如在原来的状态加一维。
分组背包
-
题目描述:有N件物品和一个容量为V的背包。第i件物品的费用是Ci,价值是Wi。这些物品被划分为K组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
-
状态:F[k,v]表示前k组物品花费费用v能取得的最大权值。
-
状态转移方程:即将问题转换为是选k组中的某一件物品,还是不选,转换为01背包问题。
F[k,v]=max{F[k−1,v],F[k−1,v−Ci]+Wi|itemi∈groupk}
-
空间优化:和01背包一致,使用一维数组。
for k ← 1 to K for v ← V to 0 for all item i in group k F[v] ← max(F[v], F[v-Ci] + Wi)
-
时间优化:可参考完全背包中的数据筛选。
有依赖的背包问题
-
问题描述:物品i依赖于物品j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。
主件:不依赖于别的物品的物品
附件:依赖于某主件的物品 -
分析
- 参考拆分物品和分组背包的思想,将一件主件和其n个附件集合做为一组,则可转换成一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件…共2^n+1个等效物品。显然需要优化!
- 参考完全背包中的时间优化第一点,对相同费用的物品进行筛选,则可将2^n+1个转换成V−Ck+1个。复杂度可接受了。
- 最后,只需将所有物品进行分组,组内进行物品拆分,运用分组背包算法即可。
-
一般化:若依赖关系是森林的形式(树形DP),即附件仍可是其他附件的主件,但不存在多重依赖和循环依赖的情况。
- 仍使用之前的算法,但将拥有附件集合的附件看作物品组,并解出其在不同容量的价值(组内实际上也是基本有依赖的背包问题);
- 将所有有附件集合的附件解出后,转换成了基本的有依赖背包问题。
树形DP:在求父节点的属性值之前,需要使用DP的方法先求得其所有子节点的所有属性
泛化物品
- 概念:没有固定的费用和价值,随分配的费用变化而变化,即给定费用c,可得到价值w = h©。
- 01背包:
if(c == Ci) h(c) = Wi; else h(c) = 0;
- 完全背包:
if(c%Ci == 0) h(c) = Wi*(v/Ci); else h(c) = 0;
- 多重背包:
if(c%Ci == 0 && v <= Ci*Mi) h(c) = Wi*(v/Ci); else h(c) = 0;
- 物品组:一个物品组即为一个泛化物品h。
if(存在费用为c的物品) h(c) = 所有费用为c的物品的最大价值; else h(c) = 0;
- 01背包:
背包问题目标的变化
以01背包为例,其他问题类似!
-
总价值的改变
- 最多可放多少件物品:所有物品的价值 = 1
- 最多可装满多少空间:物品价值 = 物品费用
- 最小:将
max
改为min
-
输出方案
- 引入G[i,v]:0 - F[i,v] = F[i−1,v];1 - F[i,v] = F[i−1,v−Ci] + Wi
- 输出结果:
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
DP问题输出方案的一般方法:记录每个状态的最优值是由状态转移方程中哪一个状态转移而来,因此当给出一个状态,可由此推出上一个状态,循环往复得到完整的方案!
-
方案总数
- 除求可得到的最大价值外,还需得到装满背包或将背包装至某一指定容量的方案总数。将max改成sum即可(初始条件是F[0,0]=1)
F[i,v] = sum{F[i−1,v],F[i,v−Ci]}
- 最优(物品总价值最大)方案的总数:引入G[i,v]表示这个子问题的最优方案的总数,则原代码修改为:
G[0,0] = 1 for i = 1 to N for j = 0 to V F[i,v] = max(F[i−1,v],F[i−1,v−Ci] + Wi) G[i,v] = 0 if F[i,v] == F[i−1,v] G[i,v] = G[i,v] + G[i-1][v] else G[i,v] = G[i,v] + G[i-1][v-Ci]
- 除求可得到的最大价值外,还需得到装满背包或将背包装至某一指定容量的方案总数。将max改成sum即可(初始条件是F[0,0]=1)
-
求次优解、第 K 优解
- 将状态转换为有序队列,将状态转移方程中的max/min/sum转换为有序队列的合并,实际上就是在原来的方程中加了一维来表示结果的优先次序。
- 状态:F[i,v] => F[i,v,1…K] 表示前i个物品中,背包大小为v时,第k优解的值
- 状态转移方程
- F[i−1][V] => F[i−1,v,1…K]
- F[i−1,v−Ci] + Wi => F[i−1,v−Ci,1…K]的每个数加上Wi
- 合并上述两个队列,并将前K项存到F[i,v,1…K]中
- 注意点:若将策略不同但权值相同的两个方案是看作同一个解,则需要保证队列中的数没有重复!