为了方便,我们叫它 DP
上次讲了 「 01 背包」 「01 背包」 「01背包」 ,这次来说说完全背包。
这里说一句, 01 背包实际上是大部分背包问题的基础,他们都是由 01 背包演化而来的。
例如这次要说的完全背包,它和 01 背包唯一不一样的就是每一个物品能取无限次。
引入例题:P1616 疯狂的采药
PS:原题是《采药》,也可以去做做看,原题是 01 背包
我们可以考虑一个朴素的做法:对于第 i 件物品,枚举其选了多少个来转移。这样做的时间复杂度是 O ( n 3 ) O(n^3) O(n3) 的。
状态转移方程如下:
f
[
i
]
[
j
]
=
max
k
=
0
+
∞
(
f
[
i
−
1
]
[
j
−
k
×
w
[
i
]
]
+
v
[
i
]
×
k
)
f[i][j]=\max_{k=0}^{+\infty}(f[i-1][j-k\times w[i]]+v[i] \times k)
f[i][j]=maxk=0+∞(f[i−1][j−k×w[i]]+v[i]×k)
考虑做一个简单的优化。可以发现,对于 f [ i ] [ j ] f[i][j] f[i][j],只要通过 f [ i ] [ j − w [ i ] ] f[i][j-w[i]] f[i][j−w[i]] 转移就可以了。因此状态转移方程为:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − w [ i ] ] + v [ i ] ) f[i][j]=max(f[i-1][j],f[i][j-w[i]]+v[i]) f[i][j]=max(f[i−1][j],f[i][j−w[i]]+v[i])
理由是当我们这样转移时, f [ i ] [ j − w [ i ] ] f[i][j-w[i]] f[i][j−w[i]] 已经由 f [ i ] [ j − 2 × w [ i ] ] f[i][j-2 \times w[i]] f[i][j−2×w[i]] 更新过,那么 f [ i ] [ j − w [ i ] ] f[i][j-w[i]] f[i][j−w[i]] 就是充分考虑了第 i i i 件物品所选次数后得到的最优结果。换言之,我们通过局部最优子结构的性质重复使用了之前的枚举过程,优化了枚举的复杂度。
与 0-1 背包相同,我们可以将第一维去掉来优化空间复杂度。
code:
#include<bits/stdc++.h>
using namespace std;
long long f[10000001],a[10000001],w[10000001];
int n,m;
int main()
{
cin>>n>>m;
for (int i=1;i<=m;i++)
{
cin>>w[i]>>a[i];
}
for (int i=1;i<=m;i++)
{
for (int j=w[i];j<=n;j++)
{
f[j]=max(f[j],f[j-w[i]]+a[i]);
}
}
cout<<f[n]<<endl;
return 0;
}
既然都说到了完全背包,那就再说说和它相似的多重背包。
他们两个唯一的不通点是,多重背包问题中一个物品能取的次数是有限的。
一个很朴素的想法就是:
把「每种物品选 k i k_i ki 次」等价转换为 「有 k i k_i ki 个相同的物品,每个物品选一次」。
这样就转换成了一个 0-1 背包模型,套用上文所述的方法就可已解决。状态转移方程如下:
f [ i ] [ j ] = max k = 0 k [ i ] ( f [ i − 1 ] [ j − k × w [ i ] ] + v [ i ] × k ) f[i][j]=\max_{k=0}^{k[i]}(f[i-1][j - k \times w[i]]+v[i] \times k) f[i][j]=maxk=0k[i](f[i−1][j−k×w[i]]+v[i]×k)
时间复杂度 O ( W ∑ i = 1 n k i ) O(W\sum_{i=1}^nk_i) O(W∑i=1nki)。
考虑优化。我们仍考虑把多重背包转化成 0-1 背包模型来求解。
显然,复杂度中的 O ( n W ) O(nW) O(nW) 部分无法再优化了,我们只能从 O ( ∑ k i ) O(\sum k_i) O(∑ki) 处入手。为了表述方便,我们用 A i , j A_{i,j} Ai,j 代表第 i i i 种物品拆分出的第 j j j 个物品。
在朴素的做法中, ∀ j ≤ k i , A i , j \forall j\le k_i,A_{i,j} ∀j≤ki,Ai,j 均表示相同物品。那么我们效率低的原因主要在于我们进行了大量重复性的工作。举例来说,我们考虑了「同时选 A i , 1 , A i , 2 A_{i,1},A_{i,2} Ai,1,Ai,2」与「同时选 A i , 2 , A i , 3 A_{i,2},A_{i,3} Ai,2,Ai,3」这两个完全等效的情况。这样的重复性工作我们进行了许多次。那么优化拆分方式就成为了解决问题的突破口。
我们可以通过「二进制分组」的方式使拆分方式更加优美。
具体地说就是令 A i , j ( j ∈ [ 0 , ⌊ log 2 ( k i + 1 ) ⌋ − 1 ] ) A_{i,j}\left(j\in\left[0,\lfloor \log_2(k_i+1)\rfloor-1\right]\right) Ai,j(j∈[0,⌊log2(ki+1)⌋−1]) 分别表示由 2 j 2^{j} 2j 个单个物品「捆绑」而成的大物品。特殊地,若 k i + 1 k_i+1 ki+1 不是 2 2 2 的整数次幂,则需要在最后添加一个由 k i − 2 ⌊ log 2 ( k i + 1 ) ⌋ − 1 k_i-2^{\lfloor \log_2(k_i+1)\rfloor-1} ki−2⌊log2(ki+1)⌋−1 个单个物品「捆绑」而成的大物品用于补足。
举几个例子:
6=1+2+3
8=1+2+4+1
18=1+2+4+8+3
31=1+2+4+8+16
显然,通过上述拆分方式,可以表示任意 ≤ k i \le k_i ≤ki 个物品的等效选择方式。将每种物品按照上述方式拆分后,使用 0-1 背包的方法解决即可。
时间复杂度 O ( W ∑ i = 1 n log 2 k i ) O(W\sum_{i=1}^n\log_2k_i) O(W∑i=1nlog2ki)
实现代码:
index = 0;
for (int i = 1; i <= m; i++) {
int c = 1, p, h, k;
cin >> p >> h >> k;
while (k > c) {
k -= c;
list[++index].w = c * p;
list[index].v = c * h;
c *= 2;
}
list[++index].w = p * k;
list[index].v = h * k;
}
好了,说了三种背包,是时候综合起来了,下面说说混合背包。
混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取 k k k 次。
这种题目看起来很吓人,可是只要领悟了前面几种背包的中心思想,并将其合并在一起就可以了。下面给出代码:
for (循环物品种类)
{
if (是 01 背包) 01 背包代码;
else if (是完全背包) 完全背包代码;
else if (是多重背包) 多重背包代码;
}
练习题:P1833 樱花