0x52
背包
背包是线性DP中一类重要而特殊的模型。
1. 0/1背包
0/1背包问题的模型如下:
给定 N N N个物品,其中第 i i i个物品的体积为 V i V_i Vi,价值为 W i W_i Wi。有一个容积为 M M M的背包,要求选择一些物品放入背包,使得物品总体积不超过 M M M的前提下,物品的价值总和最大。
根据上一节线性DP的知识,很容易想到依次考虑每个物品是否放入背包,用“已经处理的物品数”作为DP的“阶段”,以“背包中已经放入的物品总体积”作为附加维度。
F
[
i
,
j
]
F[i,j]
F[i,j]表示从前
i
i
i个物品中选出了若干物品放入体积为
j
j
j的背包,物品的最大价值和。
F
[
i
,
j
]
=
max
{
F
[
i
−
1
,
j
]
,
不选第
i
个物品
F
[
i
−
1
,
j
−
V
i
]
+
W
i
,
选第
i
个物品
F[i, j]=\max \left\{\begin{array}{l} F[i-1, j] ,不选第i个物品 \\ F[i-1, j-V_i]+W_i ,选第i个物品 \end{array}\right.
F[i,j]=max{F[i−1,j],不选第i个物品F[i−1,j−Vi]+Wi,选第i个物品
初值:未放入物品价值皆0,目标:
F
[
N
]
[
M
]
F[N][M]
F[N][M]。
memset(f,0,sizeof(f));
for(int i=1;i<=n;++i)
{
for(int j=0;j<=m;++j)
{
f[i][j]=f[i-1][j];
if(j>=v[i])
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
通过DP的状态转移方程,我们发现,每一阶段 i i i的状态只与上一个阶段 i − 1 i-1 i−1的状态有关。在这种情况下,可以使用称为“滚动数组”的优化方法,降低空间开销。
int f[2][MAX_M+1];
memset(f,0,sizeof(f));
for(int i=1;i<=n;++i)
{
for(int j=0;j<=m;++j)
{
f[i&1][j]=f[(i-1)&1][j];
if(j>=v[i])
f[i&1][j]=max(f[i&1][j],f[(i-1)&1][j-v[i]]+w[i]);
}
}
在上面的程序中,我们把阶段 i i i的状态存储在第一维下标为 i & 1 i\&1 i&1的二维数组中。当 i i i为奇数时, i & 1 i\&1 i&1等于1;当 i i i为偶数时, i & 1 i\&1 i&1等于0。因此,DP的状态就相当于在 F [ 0 ] [ ] F[0][] F[0][]和 F [ 1 ] [ ] F[1][] F[1][]两个数组中交替转移,空间复杂度从 O ( N M ) O(NM) O(NM)降低为 O ( M ) O(M) O(M)。
进一步分析发现,在每一个阶段开始时,实际上执行了一次从 F [ i − 1 ] [ ] F[i-1][] F[i−1][]到 F [ i ] [ ] F[i][] F[i][]的拷贝操作。这提示我们进一步省略掉 F F F数组的第一维,只用一维数组,即当外层循环到 i i i个物品时, F [ j ] F[j] F[j]表示背包中放入总体积为 j j j的物品的最大价值和。
int f[MAX_M+1];
memset(f,0,sizeof(f));
f[0]=0;
for(int i=1;i<=n;++i)
for(int j=m;j>=v[i];--j)
f[j]=max(f[j],f[j-v[i]]+w[i]);
请注意上面的代码片段特别标注的部分——我们使用的了倒序循环。循环到 j j j时:
1. F F F数组的后半部分 F [ j ∼ M ] F[j\sim M] F[j∼M]处于“第 i i i个阶段”,也就是已经考虑过放入第 i i i个物品的情况。
2.前半部分 F [ 0 ∼ j − 1 ] F[0\sim j-1] F[0∼j−1]处于“第 i − 1 i-1 i−1个阶段”,也就是还没有第 i i i个物品更新。
接下来 j j j不断减小,意味着我们总是用“第 i − 1 i-1 i−1个阶段”的状态向“第 i i i个阶段”的状态进行转移,符合线性DP的原则,进而保证了第 i i i个物品只会被放入背包一次。如下图所示。
然而,如果使用正序循环,假设 F [ j ] F[j] F[j]被 F [ j − V i ] + W i F[j-V_i]+W_i F[j−Vi]+Wi更新,接下来 j j j增大到 j + V i j+V_i j+Vi时, F [ j + V i ] F[j+V_i] F[j+Vi]又可能被 F [ j ] + W i F[j]+W_i F[j]+Wi更新。此时,两个都处于“第 i i i个阶段”的状态之间发生了转移,违背了线性DP的原则,相当于第 i i i个物品被使用了两次。如下图所示。
所以,在上面的代码中必须用到倒序循环,才符合0/1背包中每个物品是唯一的、只能放入背包一次的要求。
2.完全背包
完全背包问题的模型如下:
给定 N N N个物品,其中第 i i i个物品的体积为 V i V_i Vi,价值为 W i W_i Wi,并且有无数个。有一个容积为 M M M的背包,要求选择一些物品放入背包,使得物品总体积不超过 M M M的前提下,物品的价值总和最大。
先来考虑使用传统的二维线性DP的做法。设
F
[
i
,
j
]
F[i,j]
F[i,j]表示从前
i
i
i个物品中选出了若干物品放入体积为
j
j
j的背包,物品的最大价值和。
F
[
i
,
j
]
=
max
{
F
[
i
−
1
,
j
]
,
尚未选过第
i
种物品
F
[
i
,
j
−
V
i
]
+
W
i
,
i
f
j
≥
V
i
,
从第
i
种物品中选一个
F[i,j]=\max \left\{\begin{array}{l} F[i-1,j],尚未选过第i种物品 \\ F[i,j-V_i]+W_i,{ if } \ j\geq V_i,从第i种物品中选一个 \end{array} \right.
F[i,j]=max{F[i−1,j],尚未选过第i种物品F[i,j−Vi]+Wi,if j≥Vi,从第i种物品中选一个
初值:未放入物品价值皆0,目标:
F
[
N
]
[
M
]
F[N][M]
F[N][M]。
与0/1背包一样,我们也可以省略 F F F数组的 i i i这一维。根据我们在0/1背包中对循环顺序的分析,当采用正序循环时,就对应这每种物品可以使用无限次,也对应着 F [ i , j ] = F [ i , j − V i ] + W i F[i,j]=F[i,j-V_i]+W_i F[i,j]=F[i,j−Vi]+Wi这个在两个均处于 i i i阶段的状态之间进行转移的方程。
int f[MAX_M+1];
memset(f,0,sizeof(0));
for(int i=1;i<=n;++i)
{
for(int j=v[i];j<=m;++j)
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
3.多重背包
多重背包问题的模型如下:
给定 N N N个物品,其中第 i i i个物品的体积为 V i V_i Vi,价值为 W i W_i Wi,并且有 C i C_i Ci个。有一个容积为 M M M的背包,要求选择一些物品放入背包,使得物品总体积不超过 M M M的前提下,物品的价值总和最大。
直接拆分法
求解多重背包问题最直接的方法就是把第 i i i种物品看作独立的 C i C_i Ci个物品,转化为共有 ∑ i = 1 N C i \sum_{i=1}^N C_i ∑i=1NCi个物品的0/1背包问题进行计算,时间复杂度为 O ( M ∗ ∑ i = 1 N C i ) O(M*\sum_{i=1}^N C_i) O(M∗∑i=1NCi)。该算法把每种物品拆分成了 C i C_i Ci个,效率较低。
unsigned int f[MAX_M+1];
memset(f,0,sizeof(f));
for(int i=1;i<=n;++i)
for(int j=1;j<=c[i];++j)
for(int k=m;k>=v[i];--k)
f[k]=max(f[k],f[k-v[i]]+w[i]);
二进制拆分法
众所周知,从 2 0 , 2 1 , 2 2 , . . . , 2 k − 1 2^0,2^1,2^2,...,2^{k-1} 20,21,22,...,2k−1这 k k k个2的整数次幂中选出若干个相加,可以表示出 0 ∼ 2 k − 1 0\sim 2^k-1 0∼2k−1之间的任何整数。进一步地,我们求出满足 2 0 + 2 1 + . . . + 2 p ≤ C i 2^0+2^1+...+2^p \leq C_i 20+21+...+2p≤Ci的最大整数 p p p,设 R i = C i − 2 0 − 2 1 − . . . − 2 p R_i=C_i-2^0-2^1-...-2^p Ri=Ci−20−21−...−2p,那么:
1.根据 p p p的最大性,有 2 0 + 2 1 + . . . + 2 p + 1 > C i 2^0+2^1+...+2^{p+1} > C_i 20+21+...+2p+1>Ci,可推出 2 p + 1 > R i 2^{p+1}>R_i 2p+1>Ri,因此 2 0 , 2 1 , . . . , 2 p 2^0,2^1,...,2^{p} 20,21,...,2p选出若干个相加可以表示出 0 ∼ R i 0\sim R_i 0∼Ri之间的任何整数。
2.从 2 0 , 2 1 , . . . , 2 p 2^0,2^1,...,2^{p} 20,21,...,2p以及 R i R_i Ri中选出若干个相加,可以表示出 R i ∼ R i + 2 p + 1 − 1 R_i\sim R_i+2^{p+1}-1 Ri∼Ri+2p+1−1之间的任何整数,而根据 R i R_i Ri的定义, R i + 2 p + 1 − 1 = C i R_i+2^{p+1}-1=C_i Ri+2p+1−1=Ci,因此 2 0 , 2 1 , . . . , 2 p , R i 2^0,2^1,...,2^{p},R_i 20,21,...,2p,Ri选出若干个相加可以表示出 R i ∼ C i R_i\sim C_i Ri∼Ci之间的任何整数。
综上所述,我们可以把数量
C
i
C_i
Ci的第
i
i
i种物品拆成
p
+
2
p+2
p+2个物品,它们的体积分别为:
2
0
∗
V
i
,
2
1
∗
V
i
,
.
.
,
2
p
∗
V
i
,
R
i
∗
V
i
2^0*V_i,2^1*V_i,..,2^p*V_i,R_i*V_i
20∗Vi,21∗Vi,..,2p∗Vi,Ri∗Vi
这
p
+
2
p+2
p+2个物品可以凑成
0
∼
C
i
∗
V
i
0\sim C_i*V_i
0∼Ci∗Vi之间所有能被
V
i
V_i
Vi整除的数,并且不能凑成大于
C
i
∗
V
i
C_i*V_i
Ci∗Vi的数。这等价于原问题种体积为
V
i
V_i
Vi的物品可以使用
0
∼
C
i
0\sim C_i
0∼Ci次。该方法仅把每种物品拆成了
O
(
l
o
g
C
i
)
O(logC_i)
O(logCi)个,效率较高。
for(int i=1;i<=n;++i)
{
int sum=c[i];
for(int j=1;j<=sum;j*=2)
{
for(int k=m;k>=j*v[i];--k)
f[k]=max(f[k],f[k-j*v[i]]+j*w[i]);
sum-=j;
}
if(sum>0)
{
int j=sum;
for(int k=m;k>=j*v[i];--k)
f[k]=max(f[k],f[k-j*v[i]]+j*w[i]);
}
}
单调队列
使用单调队列优化的动态规划算法求解多重背包问题,时间复杂度可以进一步降低到
O
(
N
M
)
O(NM)
O(NM),与0/1背包和完全背包中的DP算法的效率相同,我们将在0x59
节中进行讲解。
4.分组背包
分组背包问题的模型如下:
给定 N N N组物品,其中第 i i i组有 C i C_i Ci个物品。第 i i i组的第 j j j个物品的体积为 V i j V_{ij} Vij,价值为 W i j W_{ij} Wij。有一容积为 M M M的背包,要求选择若干个物品放入背包,使得每组至多选择一个物品并且物品总体积不超过 M M M的前提下,物品的价值总和最大。
仍然先考虑原始线性DP的做法。为了满足“每组至多选择一个物品”,很自然的想法就是利用“阶段”线性增长的特征,把“物品组数”作为DP的“阶段”,只要使用了一个第
i
i
i组的物品,就从第
i
i
i个阶段的状态转移到第
i
+
1
i+1
i+1个阶段的状态。设
F
[
i
,
j
]
F[i,j]
F[i,j]表示从前
i
i
i组中选出物品放入总体积为
j
j
j的背包中,物品的最大价值和。
F
[
i
,
j
]
=
max
{
F
[
i
−
1
,
j
]
,
不选第
i
组的物品
max
1
≤
k
≤
C
i
F
[
i
−
1
,
j
−
V
i
k
]
+
W
i
k
,
选第
i
组的某个物品
k
F[i,j]=\max \left\{\begin{array}{l} F[i-1,j],不选第i组的物品 \\ \underset{1\leq k \leq C_i} \max F[i-1,j-V_{ik}]+W_{ik},选第i组的某个物品k \end{array} \right.
F[i,j]=max{F[i−1,j],不选第i组的物品1≤k≤CimaxF[i−1,j−Vik]+Wik,选第i组的某个物品k
与前面几个背包模型一样,我们可以省略
F
F
F数组的第一维,用
j
j
j的倒序循环来控制“阶段
i
i
i”的状态只能从“阶段
i
−
1
i-1
i−1”转移而来。
memset(f,0,sizeof(f));
for(int i=1;i<=n;++i)
for(int j=m;j>=0;j--)
for(int k=1;k<=c[i];++k)
if(j>=v[i][k])
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
除了倒序循环 j j j之外,对于每一组内 c [ i ] c[i] c[i]个物品的循环 k k k应该放在 j j j的内层。从背包的角度看,这是因为每组内至多选择一个物品,若把 k k k置于 j j j的外层,就会类似多重背包,每组物品在 F F F数组的转移上会产生累积,最终可以选择超过一个物品。从动态规划的角度, i i i是“阶段”, i i i与 j j j共同构成“状态”,而 k k k是“决策”——在第 i i i组内使用哪一个物品,这三者的顺序绝对不能混淆。
另外,分组背包是许多树形DP问题中状态转移的基本模型,在0x54
节中将进一步接触到它。
本节中,我们介绍了0/1背包、完全背包、多重背包和分组背包。除了以传统的线性DP求解之外,我们还尽量缩减了空间复杂度,省去了“阶段”的存储,用适当的循环顺序控制状态在原有基础上直接转移和累积。