有 N N N种物品和一个容量为 V V V 的背包,每种物品都有无限件可用。放入第 i i i 种物品的耗费的空间是 C i C_i Ci,得到的价值是 W i W_i Wi。求解:将哪些物品装入背包,可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
题目虽然说得有无限多个,但事实上最多装 [ V C i ] [\frac{V}{C_i}] [CiV]个了,把每种拆开,就转化成01背包问题了。
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 ≤ kCi ≤ V) F[i,v]=max{F[i−1,v−kCi]+kWi}(0≤kCi≤V)
for(int i = 1; i <= n; ++i){
for(int v = 0; v <= V; ++v){
for(int k = 0; k <= V / c[i]; ++k){
if(v >= k * c[i]) F[i][v] = max(F[i][v], F[i-1][v-k*c[i]] + k * w[i]);
}
}
}
其实第二层和第三层循环都是判定一个变量,可以优化成:
for(int i = 1; i <= n; ++i){
for(int v = c[i]; v <= V; ++v){
F[v] = max(F[v], F[v-c[i]] + w[i]);
}
}
有 N N N种物品和一个容量为 V V V 的背包。第 i i i 种物品最多有 N u m i Num_i Numi件可用,每件耗费的空间是 C i C_i Ci,价值是 W i W_i Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
这回更直接了,也不无限多个忽悠人了,就 M i M_i Mi个。
因为对于第i种物品有 M i + 1 M_i+1 Mi+1种策略:取 0 0 0件,取 1 1 1件……取 M i M_i Mi件。令 F [ i , v ] F[i,v] F[i,v]表 示前i种物品恰放入一个容量为 V V V的背包的最大价值,则有状态转移方程:
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 ≤ k ≤ M_i) F[i,v]=max{F[i−1,v−k∗Ci]+k∗Wi}(0≤k≤Mi)
和刚才差不多:
for(int i = 1; i <= n; ++i){
for(int v = 0; v <= V; ++v){
for(int k = 0; k <= num[i]; ++k){
if(v >= k * c[i]) F[i][v] = max(F[i][v], F[i-1][v-k*c[i]] + k * w[i]);
}
}
}
note:效率较低。是 O ( N × ∑ N u m × V ) O(N\times \sum Num \times V) O(N×∑Num×V)
下面看另一种思路:
如果要表示10,我们刚才是怎样的?
一个一个摆出来,像这样:
0000000000
效率较低。
于是我们想到二进制,效率最高:
1010
一样的道理,如果有 10 10 10个同种物品,那么不要拆成1111111111
,而是 1 , 2 , 4 , 3 1, 2, 4, 3 1,2,4,3个。
其中3比较特别,而 1 , 2 , 4 1, 2, 4 1,2,4是普通二进制。
每一个数都可以表示:
1 = 1
2 = 2
3 = 1+2
4 = 4
5 = 1+4
6 = 2+4
7 = 1+2+4
//已经大于1,2,4可以表示的极限了,该用上3了。
8 = 3+1+4
9 = 3+2+4
10 = 3+1+2+4
无论如何都表示的了证明嘛···略
for(int i = 1; i <= n; ++i) {
for(int k = 1; num[i]; k *= 2) {
if(num[i] < k) k = num[i] ;
num[i] -= k;
int nc = k * c[i];
int nw = k * w[i];
for(int v = V; v >= nc; --v) {
F[v] = max(F[v], F[v - nc] + nw);
}
}
}
note:效率较高。是 O ( N × log ∑ N u m × V ) O(N\times \log \sum Num \times V) O(N×log∑Num×V)
当 C i × N u m i > = V C_i \times Num_i >= V Ci×Numi>=V时,这件物品相当于完全背包,可以有一个常数优化,千万不要小看了这个优化!千万不要小看了这个优化!千万不要小看了这个优化!重要的事情说三遍。
和二进制优化一起的代码如下:
for(int i = 1; i <= n; ++i){
if (c[i] * num[i] > V) {
for(int v = c[i]; v <= V; ++v){
F[v] = max(F[v], F[v-c[i]] + w[i]);
}
continue;
}
for(int k = 1; num[i]; k *= 2){
if(num[i] < k) k = num[i] ;
num[i] -= k;
int nc = k * c[i];
int nw = k * w[i];
for(int v = V; v >= nc; --v){
F[v] = max(F[v], F[v - nc] + nw);
}
}
}
note:上面这段代码可以跟单调队列优化抗衡。
重头戏来了!单调队列优化运用于背包问题。内容较难,而且因为单调队列优化本身人丑常数大的原因。2.3 的代码完全可以和他分庭抗礼。
理论时间复杂度: O ( N × V ) O(N \times V) O(N×V)
还记得多重背包最普通的状态转移方程吗?
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\times C_i] + k\times W_i \} (0 \leq k \leq M_i) F[i,v]=max{F[i−1,v−k×Ci]+k×Wi}(0≤k≤Mi)
令 c = m i n { M i , V C i } c=min\{M_i, \frac{V}{C_i}\} c=min{Mi,CiV}
这时 0 ≤ k ≤ c 0 \leq k \leq c 0≤k≤c
容易得到 F [ i , v ] F[i,v] F[i,v]会影响 F [ i , v + k × C i ] F[i, v+k \times C_i] F[i,v+k×Ci]
如 C i = 5 C_i = 5 Ci=5时:
同一颜色的格子,对 C i C_i Ci 取模得到的余数相同。
且它们的差满足等差数列,公差为 C i C_i Ci。
通项公式为 v = k × C i + r v=k\times C_i+ r v=k×Ci+r,其中 r r r是余数。
所以我们可以根据对 C i C_i Ci 取模得到的余数进行分组。
即可分为 0 , 1 , 2 , 3 , … , C i − 1 0,1,2,3,\dots,C_i-1 0,1,2,3,…,Ci−1 共 C i C_i Ci组
且每组之间的状态转移互相影响。(相同颜色为一组)
相同颜色的格子,位置靠后的,将受到位置靠前的的影响。
如例子中, F [ 12 ] F[12] F[12] 受 F [ 7 ] F[7] F[7] 和 F [ 2 ] F[2] F[2] 的影响。
由 v = C i × ( V / C i ) + V % C i v=C_i \times (V/C_i)+V\%C_i v=Ci×(V/Ci)+V%Ci
得 v − k × C i = ( v / C i − k ) × C i + V % C i v-k \times C_i=(v/C_i-k)\times C_i+V\%C_i v−k×Ci=(v/Ci−k)×Ci+V%Ci
令 α = v / C i − k \alpha=v/C_i-k α=v/Ci−k
则方程就变成了 F [ i , v ] = m a x { F [ i − 1 , α × C i + V % C i ] − α × W i } + C i × W i F[i,v]=max\{F[i-1,\alpha \times C_i+V\%C_i]-\alpha \times W_i\}+C_i\times W_i F[i,v]=max{F[i−1,α×Ci+V%Ci]−α×Wi}+Ci×Wi
代码:
for(int i=1; i <= n; ++i) {
scanf("%d%d%d", &C, &w, &m);
c = min(m, V/C);
for(int v=0; v<C; ++j){//余数
left = 1, right = 0;
for(int k=0; k*C+v <= V; ++k){
//k-q[left]表示选取i物品的件数
while(left <= right && k-q[left] > c) left++;
//k-q[right]表示选取i物品的件数
while(left <= right && f[i-1][k*C+v] >= f[i-1][q[right]*V+v] + (k-q[right])*w) right--;
q[++right] = k;//队列中存储放的i物品个数
f[i][k*C+v] = f[i-1][q[left]*C+v] + (k-q[left])*w;
}
}
}
加上滚动数组优化,空间可以小一些。