背包问题
1 01背包
1.1题目
有 N N N件物品和一个容量为 V V V的背包,放入第 i i i件物品耗费的费用是 C i C_i Ci,得到的价值是 W i W_i Wi。求解将哪些物品放入背包可使价值总和最大。
1.2 基本思路
这是最基本的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态, F [ i ] [ v ] F[i][v] F[i][v]表示前 i i i件物品恰放入一个容量为 v v v的背包可以获得的最大价值。
其状态转移方程便是:
F
[
i
]
[
v
]
=
m
a
x
{
F
[
i
−
1
]
[
v
]
,
F
[
i
−
1
]
[
v
−
C
i
]
+
W
i
}
F[i][v]=max\{F[i-1][v],F[i-1][v-C_i]+W_i\}
F[i][v]=max{F[i−1][v],F[i−1][v−Ci]+Wi}
fin2(i,0,V) F[0][i]=0;
fin2(i,1,N)
fin2(v,C[i],V)
F[i][v]=max(F[i-1][v],F[i-1][v-C[i]]+W[i])
1.3 优化空间复杂度
以上方法的时间和空间复杂度均为 O ( N V ) O(NV) O(NV),其中时间复杂度应该已经不能在优化了,但空间复杂度却可以优化到 O ( V ) O(V) O(V)。
fin2(i,0,V) F[i]=0;
fin2(i,1,N)
fin2(v,V,C[i])
F[v]=max(F[v],F[v-C[i]]+W[i]);
从 V → C [ i ] V\to C[i] V→C[i]时, F [ v ] , F [ v − C [ i ] ] F[v],F[v-C[i]] F[v],F[v−C[i]]是 i − 1 i-1 i−1次物品时的结果。
在此抽象出一个处理一件01背包中的物品过程。
1.4 初始化的细节问题
在求最优解的背包问题题目中,有两种不太相同的问法。有的题目要求恰好装满背包时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
若要求装满背包,则在初始化时除了 F [ 0 ] F[0] F[0]为 0 0 0外,其他 F [ 1 ] → F [ n ] F[1]\to F[n] F[1]→F[n]均设为 − I N F -INF −INF。
1.5 一个常数优化
f o r i ← 1 t o N f o r v ← V t o m a x ( V − ∑ i N C i , C i ) \begin{aligned} for\ \ &i \ \leftarrow\ 1\ \ to\ \ N\\ &for\ \ v\ \leftarrow\ V\ \ to\ \ max(V-\sum_i^NC_i,C_i) \end{aligned} for i ← 1 to Nfor v ← V to max(V−i∑NCi,Ci)
视为二维转换方程更容易思考, v ← V − ∑ i n c i t o V v\leftarrow V-\sum_i^nc_i\ \ to\ \ V v←V−∑inci to V,即第 i i i个及其往后都可以被装入,只需要统计 d p [ i ] [ V − ∑ i N C i ] dp[i][V-\sum_i^NC_i] dp[i][V−∑iNCi]而无需到 d p [ C i ] dp[C_i] dp[Ci]。
1.6 引申:背包装满
给定背包容量 V V V和 n n n件物品的体积 v i v_i vi,求将背包装满有多少种方案。
子问题定义状态: d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i个件物品放入背包时占用体积正好是 j j j的方案总数。
dp[0][0]=1;
fin2(i,1,n){
fin2(j,0,V)//没选当前的
dp[i][j]=dp[i-1][j];
fin2(j,v[i],V)//选了当前的
dp[i][j]+=dp[i-1][j-v[i]];
}
outln(dp[n][V]);
1.7 数据变形
1 ≤ N ≤ 100 , 1 ≤ C i ≤ 1 0 7 , 1 ≤ W i ≤ 100 , 1 ≤ V ≤ 1 0 9 1\leq N\leq 100,1\leq C_i\leq 10^7,1\leq W_i\leq100,1\leq V\leq 10^9 1≤N≤100,1≤Ci≤107,1≤Wi≤100,1≤V≤109
此时若按照 O ( N V ) O(NV) O(NV)的做法会超时,此时我们将 d p dp dp的对象变为 W i Wi Wi, d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i个物品在价值为 j j j时所消耗重量的最小值。
d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − W i ] + C i ) dp[i][j]=min(dp[i-1][j],dp[i-1][j-W_i]+C_i) dp[i][j]=min(dp[i−1][j],dp[i−1][j−Wi]+Ci)
此时时间复杂度为 O ( N ∑ i n W i ) O(N\sum_i^nW_i) O(N∑inWi)。
2 完全背包问题
2.1 题目
有 N N N种花费和价值分别为 C i , W i C_i,W_i Ci,Wi的物品,从这些物品中挑选总体积不超过 V V V的物品,求出挑选物品价值总和的最大值。在这里,每种商品可以挑选任意多件。
2.2 基本思路
F [ i ] [ v ] = m a x { F [ i − 1 ] [ v − k C i ] + K W i ∣ 0 ≤ k C i ≤ v } F[i][v]=max\{F[i-1][v-kC_i]+KW_i|0\leq kC_i\leq v\} F[i][v]=max{F[i−1][v−kCi]+KWi∣0≤kCi≤v}
求解每个 F [ i ] [ v ] F[i][v] F[i][v]的时间为 O ( v C i ) O({v\over C_i}) O(Civ),总的时间复杂度为 O ( N V ∑ V C i ) O(NV\sum{V\over C_i}) O(NV∑CiV)。
2.3 一个简单有效的优化
若两件物品 i 、 j i、j i、j满足 C i ≤ C j C_i\leq C_j Ci≤Cj且 W i ≥ W j W_i\geq W_j Wi≥Wj,则可以将物品 j j j直接去掉。这个优化可以简单用 O ( N 2 ) O(N^2) O(N2)地实现。
另一种不错的方法是,首先将费用大于
V
V
V的物品去掉,然后
w
e
i
g
h
t
[
i
]
weight[i]
weight[i](可以用map
实现)表示重量为
i
i
i的物品中价值最高的那个,可以
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)地完成这个优化。
2.4 转化为01背包问题求解
最简单的想法是:考虑第 i i i种物品最多选 ⌊ V / C i ⌋ \lfloor V/C_i\rfloor ⌊V/Ci⌋件,于是可以把第 i i i种物品转化为 ⌊ V / C i ⌋ \lfloor V/C_i\rfloor ⌊V/Ci⌋件费用和价值均不变的物品,然后求解这个01背包的问题。这样的做法完全没有改进时间复杂度,但这种方法指明了完全背包问题转化为01背包问题的思路:将一种物品拆成多件只能选 0 0 0件或 1 1 1件的01背包中的物品。
更为高效的转换方法是:将第 i i i种物品拆成费用为 C i 2 k C_i2^k Ci2k、价值为 W i 2 k W_i2^k Wi2k的若干物品,其中 k k k取遍满足 C i 2 k ≤ V C_i2^k\leq V Ci2k≤V的非负整数。
这是二进制思想。因为不管最优策略选几件第 i i i种物品,其件数写成二进制后,总可以表示成由若干个 2 k 2^k 2k件物品的总和。这样就把每种物品拆成 O ( l o g ⌊ V / C i ⌋ ) O(log\lfloor V/C_i\rfloor) O(log⌊V/Ci⌋)件物品,是一个很大的改进。
2.5 O ( V N ) O(VN) O(VN)的算法
fin2(i,0,V) F[i]=0;
fin2(i,1,N)
fin2(v,C[i],V)
F[v]=max(F[v],F[v-C[i]]+W[i]);
这个伪代码与01背包问题只有 v v v的循环次序不同而已。
为什么这个算法就可行呢?因为01背包中要确保每件物品只能选一次,在考虑“选入第 i i i件物品”这种策略时,依据的是一个绝无已经选入第 i i i件物品的子
结果 F [ i − 1 ] [ v − C i ] F[i-1][v-C_i] F[i−1][v−Ci]。而完全背包的特点恰是每件物品可以选无限件,所以在考虑“加选一件第 i i i种物品”这种策略时,正需要一个可能已经选入第 i i i种物品的子结果 F [ i ] [ v − C i ] F[i][v-C_i] F[i][v−Ci],所以需要采用 v v v递增的顺序循环。
3 多重背包问题
3.1 题目
有 N N N种物品和一个容量为 V V V的背包。第 i i i种物品最多有 M i M_i Mi件可用,每件耗费的空间是 C i C_i Ci,价值是 W i W_i Wi。求解将哪些物品放入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
3.2 基本算法
F [ i ] [ v ] = m a x { F [ i − 1 ] [ v − k ∗ C i ] + k ∗ W i ∣ 0 ≤ k ≤ M i } F[i][v]=max\{F[i-1][v-k*C_i]+k*W_i|0\leq k\leq M_i\} F[i][v]=max{F[i−1][v−k∗Ci]+k∗Wi∣0≤k≤Mi}
复杂度是 O ( V ∑ M i ) O(V\sum M_i) O(V∑Mi)。
3.3 转化为01背包问题
将第 i i i种物品分成若干件01背包中的物品,其中每一件物品有一个系数。分别为 2 0 , 2 1 , 2 2 . . . 2 k − 1 , M i − 2 k + 1 2^0,2^1,2^2...2^k-1,M_i-2^k+1 20,21,22...2k−1,Mi−2k+1,且 k k k是满足 M i − 2 k + 1 > 0 M_i-2^k+1>0 Mi−2k+1>0的最大整数。
这样就转化为了复杂度为
O
(
l
o
g
M
)
O(logM)
O(logM)时间处理一件多次背包中物品的过程:
d
e
f
M
u
l
t
i
p
l
e
P
a
c
k
(
F
,
C
,
W
,
M
)
i
f
C
∗
M
≥
V
C
o
m
p
l
e
P
a
c
k
(
F
,
C
,
V
)
r
e
t
u
r
n
k
←
1
w
h
i
l
e
k
<
M
Z
e
r
o
O
n
e
P
a
c
k
(
k
C
,
k
W
)
M
←
M
−
k
k
←
2
∗
k
Z
e
r
o
O
n
e
P
a
c
k
(
M
∗
C
,
M
∗
W
)
\begin{aligned} def \ \ Mul&tiplePack(F,C,W,M)\\ if\ \ &C*M\geq V\\ &ComplePack(F,C,V)\\ &return\\ k\leftarrow& 1\\ whil&e \ k<M\\ &ZeroOnePack(kC,kW)\\ &M\leftarrow M-k\\ &k\leftarrow 2*k\\ Zero&OnePack(M*C,M*W) \end{aligned}
def Mulif k←whilZerotiplePack(F,C,W,M)C∗M≥VComplePack(F,C,V)return1e k<MZeroOnePack(kC,kW)M←M−kk←2∗kOnePack(M∗C,M∗W)
3.4 可行性问题 O ( V N ) O(VN) O(VN)的算法
当问题是“每种有若干件的物品是否能填满给定容量背包”,只需考虑能否填满背包,不需考虑每件物品的价值时,多重背包同样有 O ( V N ) O(VN) O(VN)复杂度的算法。(比较1.6)
设: F [ i ] [ j ] F[i][j] F[i][j]表示“用了前 i i i种物品填满容量为 j j j的背包后,最多还剩下几个第 i i i种物品可用”,如果 F [ i ] [ j ] = − 1 F[i][j]=-1 F[i][j]=−1则说明这种状态不可行,若可行应满足 0 ≤ F [ i ] [ j ] ≤ M i 0\leq F[i][j]\leq M_i 0≤F[i][j]≤Mi。
fin2(i,1,V) F[0][i]=-1;
F[0][0]=0;
fin2(i,1,N)
fin2(j,0,V)
if(F[i-1][j]>=0)
F[i][j]=M[i]//此时相当于一件i都还没有用,初始化F[i][j]也是下面max()函数的原因
else
F[i][j]=-1
fin2(j,0,V-C[i])
if(F[i][j]>0)//剩余件数大于0时就可以更新F[i][j+C[i]]
F[i][j+C[i]]=max(F[i][j+C[i]],F[i][j]-1)
最终 F [ N ] [ V ] F[N][V] F[N][V]便是多重背包可行性的答案。
4 混合三种背包问题
4.1 问题
如果将前面1、2、3中的三种背包问题混合起来。也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?
4.2 抽象出过程的代码
int pack[i];//i=0表示完全背包
int f[MAXV];
int c[MAXN],w[MAXN];
void ZeroOnePack(int i){
for(int j=V;j>=c[i];j--)
f[j]=max(f[j],f[j-c[i]]+w[i]);
}
void CompletePack(int i){
for(int j=c[i];j<=V;j++)
f[j]=max(f[j],f[j-c[i]]+w[i]);
}
void MultiplePack(int i){
if(c[i]*pack[i]>=V){
CompletePack(i);
return ;
}
int k=1;
int ans_c=c[i],ans_w=w[i];
while(k<pack[i]){
ZeroOnePack(i);
c[i]*=2;w[i]*=2;
pack[i]-k;k*=2;
}
c[i]=pack[i]*ans_c;
w[i]=pack[i]*ans_w;
ZeroOnePack(i);
}
int main(){
for(int i=1;i<=n;i++){
if(pack[i]==1) ZeroOnePack(i);
else if(pack[i]==0) CompletePack(i);
else MultiplePack(i);
}
cout<<f[V]<<endl;
}
5 二维费用的背包问题
5.1 问题
二维费用的背包问题是指:对于每件物品,具有两种不同的费用,选择这件物品必须同时付出这两种费用。对于每种费用都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。
设第 i i i件物品所需要的两种费用分别为 C i C_i Ci和 D i D_i Di。两种费用可付出的最大值(也即两种背包容量)分别为 V V V和 U U U。物品的价值为 W i W_i Wi。
5.2 算法
费用加了一维,只需状态也加了一维即可。设
F
[
i
]
[
v
]
[
u
]
F[i][v][u]
F[i][v][u]表示前
i
i
i件物品付出两种费用分别为
v
v
v和
u
u
u时可获得的最大价值。状态转移方程就是:
F
[
i
]
[
v
]
[
u
]
=
m
a
x
{
F
[
i
−
1
]
[
v
]
[
u
]
,
F
[
i
−
1
]
[
v
−
C
i
]
[
u
−
D
i
]
+
W
i
}
F[i][v][u]=max\{F[i-1][v][u],F[i-1][v-C_i][u-D_i]+W_i\}
F[i][v][u]=max{F[i−1][v][u],F[i−1][v−Ci][u−Di]+Wi}
如前述优化空间复杂度的方法,可以只使用二维的数组:当每件物品只可以取一次时变量
v
v
v和
u
u
u采用逆序的循环,当物品有如完全背包问题时采用顺序的循环,当物品有如多重背包时拆分物品。
5.3 物品总个数的限制
有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取 U U U件物品,这事实上相当于每件物品多了一种“件数”的费用。
5.4 小结
当发现由熟悉的动态规划题目变形得来的题目时,在原来的状态中加一维以满足新的限制是一种比较通用的方法。
6 分组的背包问题
6.1 问题
有 N N N件物品和一个容量为 V V V的背包。第 i i i件物品的费用是 C i C_i Ci,价值是 W i W_i Wi。这些物品被划分成 K K K组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
6.2 算法
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设
F
[
k
]
[
v
]
F[k][v]
F[k][v]为表示前
k
k
k组物品花费费用
v
v
v能取得的最大值。
F
[
k
]
[
v
]
=
m
a
x
{
F
[
k
−
1
]
[
v
]
,
F
[
k
−
1
]
[
v
−
C
i
]
+
W
i
∣
i
t
e
m
i
∈
g
r
o
u
p
k
}
F[k][v]=max\{F[k-1][v],F[k-1][v-C_i]+W_i|item\ i\in group\ k\}
F[k][v]=max{F[k−1][v],F[k−1][v−Ci]+Wi∣item i∈group k}
使用一维数组的伪代码如下:
fin2(k,1,K)
fin2(v,V,0)
for all item i in group k
F[v]=max(F[v],F[v-C[i]]+W[i])
复杂度为 O ( N V ) O(NV) O(NV),可以对每组内的物品应用2.3中的优化。
6.3 小结
分组的背包问题将彼此互斥的若干物品分为一个组,这将建立一个很好的模型。不少背包问题的变形都可以转化为分组的背包问题,由分组的背包问题进一步可定义“泛化物品”的概念,十分有利于解题。
7 有依赖的背包问题
7.1 简化的问题
这种背包问题的物品间存在某种“依赖”的关系。也就是说,物品 i i i依赖于物品 j j j,表示若选物品 i i i,则必须选物品 j j j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。
7.2 算法
我们将不依赖于其他物品的物品称为“主件”,依赖于某主件的物品称为“附件”。可知所有的物品由若干主件和依赖于每个主件的一个附件集合组成。
一个主件和它的附件集合实际上对应于 6 6 6中的一个物品组,每个选择了主件又选择了若干个附件的策略对应于这个物品组中的一个物品,其费用和价值都是这个策略中的物品的值的和。
我们考虑对每组内的物品应用 2.3 2.3 2.3中的优化,对于第 k k k个物品组中的物品,所有费用相同的物品只保留一个价值最大的,不影响结果。所以,可以对主件 k k k的附件集合先进行一次 01 01 01背包,得到费用依次为 0 , . . . , V − C k 0,...,V-C_k 0,...,V−Ck时对应的最大价值 F k [ 0 , . . . , V − C k ] F_k[0,...,V-C_k] Fk[0,...,V−Ck]。那么,这个主件及它的附件集合相当于 V − C k + 1 V-C_k+1 V−Ck+1个物品组,其中费用为 v v v的物品价值为 F k [ c − C k ] + W k F_k[c-C_k]+W_k Fk[c−Ck]+Wk, v v v的取值范围是 C k ≤ v ≤ V C_k\leq v\leq V Ck≤v≤V。
也就是说,在原来指数级的策略中,有很多策略都是冗余的,通过一次 01 01 01背包后,将主件 k k k及其附件转化为 V − C k + 1 V-C_k+1 V−Ck+1个物品的物品组,就可以直接应用 6 6 6的算法解决问题了。
/*洛谷 P1064 金明的预算方案*/
const int MAX_V=32005;
const int MAX_N=65;
int c[MAX_N];//cost
int w[MAX_N];//weight
int rely[MAX_N];//依赖关系
vector<int> group[MAX_N];//group[i]表示依赖于i的附件集合
int group_f[MAX_N][MAX_V];//group_f[k][]表示第k组01背包后的结果
vector<pint> group_item[MAXN];//group[k]表示第k组物品组的中的物品
int total_f[MAX_V];//最终分组背包的结果
int V,n;
void ZeroOnePack(int k){
if(group[k].size()){//判断有无附件,因为group[k].size()的返回类型是无符号整数
fin2(i,0,group[k].size()-1)
for(int j=V-c[k];j>=c[group[k][i]];j--)
group_f[k][j]=max(group_f[k][j],group_f[k][j-c[group[k][i]]]+w[group[k][i]]);
int ans=0;
fin2(i,0,V-c[k]){
if(group_f[k][i]>ans) {//weight一样时,取cost最小的
group_item[k].push_back(mk(i+c[k],group_f[k][i]+w[k]));
ans=group_f[k][i];
}
}
}
group_item[k].push_back(mk(c[k],w[k]));
}
void GroupPack(){
fin2(k,1,n){//遍历物品组
if(rely[k]) continue;
for(int v=V;v>=0;v--){
fin2(i,0,group_item[k].size()-1){//遍历每一个物品
if(group_item[k][i].first>v) continue;//重要
total_f[v]=max(total_f[v],total_f[v-group_item[k][i].first]+group_item[k][i].second);
}
}
}
}
int main(){
in(V);in(n);
fin2(i,1,n){
in(c[i]);in(w[i]);in(rely[i]);
w[i]=w[i]*c[i];
group[rely[i]].push_back(i);
}
fin2(i,1,n){
if(rely[i]) continue;
/*对group数组内的数进行一次01背包*/
if(c[i]<=V) ZeroOnePack(i);
}
GroupPack();
outln(total_f[V]);
return 0;
}
7.3 更一般的问题
更一般的问题是:依赖关系以图论中**“森林”**的形式给出。也就是说,主件的附件仍然为可以具有自己的附件集合。限制只是每个物品最多只依赖于一个物品(只有一个主件)且不出现循环依赖。
这是一种树形动态规划,其特点是,在用动态规划求每个父节点的属性之前,需要对它的各个儿子的属性进行一次动态规划求值。这已经触及到了“泛化物品”的思想。看完 8 8 8后,你会发现这个“依赖关系树”每个子树都等价于一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的对应的泛化物品之和。
8 泛化物品
8.1 定义
考虑这样一件物品,它没有固定的费用和价值,而是它的价值随着你分配给它的费用而变化。这就是泛化物品的概念。
在背包容量为 V V V的背包问题中,泛化物品就是一个定义域在 [ 0 , V ] [0,V] [0,V]中的整数的函数 h h h,当分配给它的费用为 v v v时,能得到的价值就是 h ( v ) h(v) h(v)。
另一种理解是一个泛化物品就是一个数组 h [ 0 , . . . , V ] h[0,...,V] h[0,...,V],给它一个费用 v v v,可得到的价值为 h [ v ] h[v] h[v]。
一个费用为 c c c价值为 w w w的物品,如果它是 01 01 01背包中的物品,那么把它看成泛化物品,它就是除了 h ( c ) = w h(c)=w h(c)=w外,其他函数都为 0 0 0的一个函数;如果它是完全背包中的物品,可视为仅当 v v v被 c c c整除时有 h ( v ) = w ∗ v c h(v)=w*{v\over c} h(v)=w∗cv,其他函数值均为 0 0 0;如果它是多重背包中的物品,可视为仅当 v v v被 c c c整除且 v c ≤ m {v\over c}\leq m cv≤m(件数)时 h ( v ) = w ∗ v c h(v)=w*{v\over c} h(v)=w∗cv,其他函数值均为 0 0 0。
一个物品组可以看作一个泛化物品 h h h。对于一个 [ 0 , V ] [0,V] [0,V]中的 v v v,若物品组中不存在费用为 v v v的物品,则 h ( v ) = 0 h(v)=0 h(v)=0,否则 h ( v ) h(v) h(v)的取值为所有费用为 v v v的物品的最大价值。 7 7 7中的每个主件及其附件集合等价于一个物品组,自然可以看作一个泛化物品。
8.2 泛化物品的和
如果给定了两个泛化物品
h
h
h和
l
l
l,要用一定的费用从这两个泛化物品中得到最大的价值,这个问题怎么求呢?可以对于每一个给定的费用
v
v
v,只需枚举将这个费用如何分配给两个泛化物品就可以了。因此,对于
[
0
,
V
]
[0,V]
[0,V]中的每一个整数
v
v
v,可以求得费用
v
v
v分配到
h
h
h和
l
l
l中的最大价值
f
(
v
)
f(v)
f(v)。也即
f
(
v
)
=
m
a
x
{
h
(
k
)
+
l
(
v
−
k
)
∣
0
≤
k
≤
v
}
f(v)=max\{h(k)+l(v-k)|0\leq k\leq v\}
f(v)=max{h(k)+l(v−k)∣0≤k≤v}
可以看到,这里的
f
f
f是一个由泛化物品
h
h
h和
l
l
l决定的定义域为
[
0
,
V
]
[0,V]
[0,V]的函数,也就是说,
f
f
f是一个由泛化物品
h
h
h和
l
l
l决定的泛化物品,定义为泛化物品
h
h
h和
l
l
l的和。
泛化物品和运算的时间复杂度取决于背包容量,是 O ( V 2 ) O(V^2) O(V2)。
(个人目前理解,对多个泛化物品的求解,还是使用分组背包求解方法的时间复杂度更低)
9 背包问题问法的变化
9.1 输出方案
如果要求输出这个最优值的方案,可以参考一般动态规划问题输出方案的方法:记录下每个状态的最优值是由状态转移方程的哪一项推出来的。
以
01
01
01背包为例,输出方案的伪代码可以这样写(设最终状态为
F
[
N
]
[
V
]
F[N][V]
F[N][V])
i
←
N
v
←
V
w
h
i
l
e
i
>
0
i
f
F
[
i
]
[
v
]
=
F
[
i
−
1
]
[
v
]
p
r
i
n
t
未
选
第
i
项
物
品
e
l
s
e
i
f
F
[
i
]
[
v
]
=
F
[
i
−
1
]
[
v
−
C
[
i
]
]
+
W
[
i
]
p
r
i
n
t
选
了
第
i
项
物
品
v
←
v
−
C
[
i
]
i
←
i
−
1
\begin{aligned} i\leftarrow N& \\ v\leftarrow V& \\ while \ \ &i>0\\ if& \ \ F[i][v]=F[i-1][v]\\ &\ \ \ \ print \ \ 未选第i项物品\\ el&se \ \ if \ \ F[i][v]=F[i-1][v-C[i]]+W[i]\\ &\ \ \ \ print \ \ 选了第i项物品\\ &\ \ \ \ v\leftarrow v-C[i]\\ i&\leftarrow i-1 \end{aligned}
i←Nv←Vwhile ifelii>0 F[i][v]=F[i−1][v] print 未选第i项物品se if F[i][v]=F[i−1][v−C[i]]+W[i] print 选了第i项物品 v←v−C[i]←i−1
9.2 输出字典序最小的最优方案
先将物品编号做 x ← N + 1 − x x\leftarrow N+1-x x←N+1−x的变换,在输出方案时再变换回来。在做完物品编号的变换后,可以按照前面经典的转移方程来求值。只是在输出方案时要注意,如果 F [ i ] [ v ] = F [ i − 1 ] [ v ] F[i][v]=F[i-1][v] F[i][v]=F[i−1][v]和 F [ i ] [ v ] = F [ i − 1 ] [ v − C [ i ] ] + W [ i ] F[i][v]=F[i-1][v-C[i]]+W[i] F[i][v]=F[i−1][v−C[i]]+W[i]都成立,应该按照后者来输出方案,即选择了物品 i i i,输出其原来的编号 N + 1 − i N+1-i N+1−i。
9.3 求方案总数
对于一个给定了背包容量、物品费用、物品间依赖关系(分组、依赖等)的背包问题,除了再给定每个物品的价值后求可的到的最大价值外,还可以得到装满背包或将背包装至某一指定容量的方案总数。
对于这类改变问法的问题,一般只需要将状态转移方程中的
m
a
x
max
max改成
s
u
m
sum
sum即可。例如若每件物品均是完全背包中的物品,转移方程即为
F
[
i
]
[
v
]
=
s
u
m
{
F
[
i
−
1
]
[
v
]
,
F
[
i
]
[
v
−
C
i
]
}
F[i][v]=sum\{F[i-1][v],F[i][v-C_i]\}
F[i][v]=sum{F[i−1][v],F[i][v−Ci]}
初始条件是
F
[
0
]
[
0
]
=
1
F[0][0]=1
F[0][0]=1。
9.4 最优方案总数
这里的最优方案是指物品总价值最大的方案。以 01 01 01背包为例。
G
[
i
]
[
v
]
G[i][v]
G[i][v]表示这个子问题的最优方案的总数。
G
[
0
]
[
0
]
=
1
f
o
r
i
←
1
t
o
N
f
o
r
v
←
0
t
o
V
F
[
i
]
[
v
]
←
m
a
x
{
F
[
i
−
1
]
[
v
]
,
F
[
i
−
1
]
[
v
−
C
i
]
+
W
i
}
G
[
i
]
[
v
]
=
0
;
i
f
F
[
i
]
[
v
]
=
F
[
i
−
1
]
[
v
]
G
[
i
]
[
v
]
←
G
[
i
]
[
v
]
+
G
[
i
−
1
]
[
v
]
i
f
F
[
i
]
[
v
]
=
F
[
i
−
1
]
[
v
−
C
i
]
+
W
i
G
[
i
]
[
v
]
←
G
[
i
]
[
v
]
+
G
[
i
−
1
]
[
v
−
C
i
]
\begin{aligned} &G[0][0]=1\\ &for \ \ i\leftarrow 1 \ \ to \ \ N\\ &\ \ \ \ for\ \ v\leftarrow 0\ \ to\ \ V\\ &\ \ \ \ \ \ \ \ F[i][v]\leftarrow max\{F[i-1][v],F[i-1][v-C_i]+W_i\}\\ &\ \ \ \ \ \ \ \ G[i][v]=0;\\ &\ \ \ \ \ \ \ \ if\ \ F[i][v]=F[i-1][v]\\ &\ \ \ \ \ \ \ \ \ \ \ \ G[i][v]\leftarrow G[i][v]+G[i-1][v]\\ &\ \ \ \ \ \ \ \ if\ \ F[i][v]=F[i-1][v-C_i]+W_i\\ &\ \ \ \ \ \ \ \ \ \ \ \ G[i][v]\leftarrow G[i][v]+G[i-1][v-C_i]\\ \end{aligned}
G[0][0]=1for i←1 to N for v←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] if F[i][v]=F[i−1][v−Ci]+Wi G[i][v]←G[i][v]+G[i−1][v−Ci]
其核心是两个
i
f
if
if语句,同时成立时方案数合并,否则选择其中一个继承。
9.5 求次优解、第K优解
对于求解最优解、第 K K K优解类的问题,如果相应的最优解问题能写出状态转移方程、用动态规划解决,那么求次优解往往可以用相同的时间复杂度解决,第K优解则比最优解的复杂度上多了一个系数 K K K。
其基本思想是,将每个状态都要表示成有序队列,将状态转移方程中的 m a x / m i n max/min max/min转换成有序队列的合并。
这里仍然以 01 01 01背包为例。
首先看一下 01 01 01背包求解最优解的状态转移方程: F [ i ] [ v ] = m a x { F [ i − 1 ] [ v ] , F [ i − 1 ] [ v − C i ] + W i } F[i][v]=max\{F[i-1][v],F[i-1][v-C_i]+W_i\} F[i][v]=max{F[i−1][v],F[i−1][v−Ci]+Wi}。如果要求第 K K K优解,那么状态 F [ i ] [ v ] F[i][v] F[i][v]就应该是是一个大小为的队列 K K K, F [ i ] [ v ] [ 1 , . . . , K ] F[i][v][1,...,K] F[i][v][1,...,K]。其中 F [ i ] [ v ] [ k ] F[i][v][k] F[i][v][k]表示前 i i i个物品中,背包大小为 v v v时,第 k k k优解的值。这里也可以简单地理解为在原来的方程中加了一维来表示结果的优先次序。显然 F [ i ] [ v ] [ 1 , . . . , K ] F[i][v][1,...,K] F[i][v][1,...,K]这 K K K个数是有小到大排序的,所以它可以看作是一个有序队列。
然后原方程就可以解释为: F [ i ] [ v ] F[i][v] F[i][v]这个有序队列是由 F [ i − 1 ] [ v ] F[i-1][v] F[i−1][v]和 F [ i − 1 ] [ v − C i ] + W i F[i-1][v-C_i]+W_i F[i−1][v−Ci]+Wi这两个有序队列合并得到的。合并这两个有序队列并将结果的前 K K K项存储到 F [ i ] [ v ] [ 1 , . . . , K ] F[i][v][1,...,K] F[i][v][1,...,K]中的复杂度为 O ( K ) O(K) O(K)。最后的求解第 K K K优解的答案是 F [ N ] [ V ] [ K ] F[N][V][K] F[N][V][K]。总的时间复杂度是 O ( V N K ) O(VNK) O(VNK)。
另外需要注意题目对于“第 K K K优解”的定义,是要求将策略不同但权值相同的方案看作是同一解还是不同解。如果是前者,则维护有序队列时要保证队列里的数是没有重复的。
10 后记
本文可视为学习背包九讲教程的笔记,在此表示对原作者的感谢。
希望以后遇到背包问题时可以顺利解决,遇到好的问题时会在文中进行补充。