之前打 ABC 发现自己背包掌握的不是很熟练,所以写这一篇复习笔记。
基本模型
有 n n n 种物品和一个容量为 m m m 的背包,每种物品有重量 w i w_i w i 与价值 v i v_i v i 两种属性,要求选若干个物品,使它们的重量之和 ≤ m \le m ≤ m 且 ∑ v \sum v ∑ v 最大。
各种背包都是在这个模型下的。
01 背包
每种物品有且仅有一个。
设 d p i , j dp_{i, j} d p i , j 表示前 i i i 种物品,容量为 j j j 的背包最大价值。
则
d p i , j = max ( d p i − 1 , j , d p i − 1 , j − w i + v i ) dp_{i, j}=\max(dp_{i-1,j}, dp_{i-1,j-w_i}+v_i)
d p i , j = max ( d p i − 1 , j , d p i − 1 , j − w i + v i )
这里的两种选择对应选 / 不选第 i i i 种物品。
可以滚动数组优化。内层循环需要 j j j 从 m m m 逆向枚举到 w i w_i w i ,防止 d p j dp_j d p j 被 d p j − w i dp_{j-w_i} d p j − w i 影响。
如此,时间 O ( n m ) O(nm) O ( nm ) ,空间 O ( m ) O(m) O ( m ) 。
板子一个。代码如下:
1 2 3 4 5 6 7 8 9 10 11 const int N = 13000 ;int n, m, w[N], v[N], dp[N];void _main() { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> w[i] >> v[i]; for (int i = 1 ; i <= n; i++) { for (int j = m; j >= w[i]; j--) dp[j] = max (dp[j], dp[j - w[i]] + v[i]); } cout << dp[m]; }
这题与普通背包的区别在于一种物品有两个体积,在 d p dp d p 数组中多开一维即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 const int N = 205 ;int n, m1, m2, w1[N], w2[N], dp[N][N];void _main() { cin >> n >> m1 >> m2; for (int i = 1 ; i <= n; i++) cin >> w1[i] >> w2[i]; for (int i = 1 ; i <= n; i++) { for (int j = m1; j >= w1[i]; j--) { for (int k = m2; k >= w2[i]; k--) dp[j][k] = max (dp[j][k], dp[j - w1[i]][k - w2[i]] + 1 ); } } cout << dp[m1][m2]; }
01 背包简单变形。仍然沿用 d p dp d p 状态,则
d p j = max ( d p j + l o s e i , d p j − u s e i + w i n i ) dp_j=\max(dp_j+lose_i,dp_{j-use_i}+win_i)
d p j = max ( d p j + l os e i , d p j − u s e i + w i n i )
注意 j < u s e i j < use_i j < u s e i 时第二个转移无法进行。
1 2 3 4 5 6 7 8 9 10 11 const int N = 1e3 + 5 ;int n, m, v1[N], v2[N], w[N], dp[N];void _main() { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v2[i] >> v1[i] >> w[i]; for (int i = 1 ; i <= n; i++) { for (int j = m; j >= 0 ; j--) dp[j] = max (dp[j] + v2[i], j < w[i] ? 0 : dp[j - w[i]] + v1[i]); } cout << 5LL * dp[m]; }
三种 V i V_i V i 分开跑 01 背包,暴力枚举两种 V i V_i V i 所占的体积,合并答案即可。复杂度 O ( n x + n 2 ) O(nx+n^2) O ( n x + n 2 ) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const int N = 5005 ;int n, x, opt;int n1, n2, n3;struct node { int v, w; } a1[N], a2[N], a3[N];int dp1[N], dp2[N], dp3[N];void backpack01 (node* a, int n, int * dp) { for (int i = 1 ; i <= n; i++) { for (int j = x; j >= a[i].w; j--) dp[j] = max (dp[j], dp[j - a[i].w] + a[i].v); } }void _main() { cin >> n >> x; for (int i = 1 ; i <= n; i++) { cin >> opt; if (opt == 1 ) n1++, cin >> a1[n1].v >> a1[n1].w; if (opt == 2 ) n2++, cin >> a2[n2].v >> a2[n2].w; if (opt == 3 ) n3++, cin >> a3[n3].v >> a3[n3].w; } backpack01 (a1, n1, dp1); backpack01 (a2, n2, dp2); backpack01 (a3, n3, dp3); int res = 0 ; for (int i = 1 ; i <= x; i++) { for (int j = 1 ; j <= x; j++) { int k = x - i - j; if (k < 0 ) continue ; res = max (res, min (dp1[i], min (dp2[j], dp3[k]))); } } cout << res; }
把智商当体积,情商当价值,跑完 01 背包合并答案即可。为了处理负数,进行一个数组漂移的 trick。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const int N = 1e5 ;int n, w[500 ], v[500 ], dp[(N << 2 ) + 5 ];void _main() { cin >> n; for (int i = 1 ; i <= n; i++) cin >> w[i] >> v[i]; memset (dp, 0xcf , sizeof (dp)); dp[N] = 0 ; for (int i = 1 ; i <= n; i++) { if (w[i] >= 0 ) for (int j = (N << 1 ); j >= w[i]; j--) dp[j] = max (dp[j], dp[j - w[i]] + v[i]); else for (int j = 0 ; j <= (N << 1 ) + w[i]; j++) dp[j] = max (dp[j], dp[j - w[i]] + v[i]); } int res = 0 ; for (int i = N; i <= (N << 1 ); i++) { if (dp[i] >= 0 ) res = max (res, i - N + dp[i]); } cout << res; }
背包经典变式。
模 m m m 同余类的背包问题,一般设 d p i , j dp_{i,j} d p i , j 表示前 i i i 种物品,总价值模 m m m 等于 j j j 。这里 d p i , j dp_{i,j} d p i , j 表示方案数。
那么有转移
d p i , j = d p i − 1 , j + d p i − 1 , ( j − w i ) m o d m dp_{i,j}=dp_{i-1,j}+dp_{i-1,(j-w_i)\bmod m}
d p i , j = d p i − 1 , j + d p i − 1 , ( j − w i ) mod m
会发现其实和普通 01 背包的区别就是多模了个 m m m 。一年前写的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const int mod = 1e8 ;int n, k, a[N], dp[N][N];void _main() { cin >> n >> k; for (int i = 1 ; i <= n; i++) cin >> a[i]; for (int i = 1 ; i <= n; i++) dp[i][a[i] % k] = 1 ; for (int i = 1 ; i <= n; i++) { for (int j = 0 ; j < k; j++) { dp[i][j] = (dp[i][j] + dp[i - 1 ][j]) % mod; dp[i][j] = (dp[i][j] + dp[i - 1 ][(j - a[i] % k + k) % k]) % mod; } } cout << dp[n][0 ]; return 0 ; }
直接跑背包计数,复杂度是 O ( n 2 m ) O(n^2m) O ( n 2 m ) 的。正解用到背包的撤销 trick。
作转移 d p j ← d p j + d p j − w i dp_j \gets dp_j+dp_{j-w_i} d p j ← d p j + d p j − w i 时,我们加入了物品 i i i 。那么删去物品 i i i 就是作 d p j ← d p j − d p j − w i dp_j \gets dp_j-dp_{j-w_i} d p j ← d p j − d p j − w i 。代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const int N = 2005 ;int n, m, w[N], dp1[N], dp2[N];void _main() { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> w[i]; dp1[0 ] = 1 ; for (int i = 1 ; i <= n; i++) { for (int j = m; j >= w[i]; j--) { dp1[j] += dp1[j - w[i]]; dp1[j] %= 10 ; } } for (int i = 1 ; i <= n; i++) { memcpy (dp2, dp1, sizeof (dp1)); for (int j = w[i]; j <= m; j++) { dp2[j] -= dp2[j - w[i]]; dp2[j] = (dp2[j] + 10 ) % 10 ; } for (int j = 1 ; j <= m; j++) cout << dp2[j]; cout << '\n' ; } return 0 ; }
01 背包,k k k 优解问题。
考虑多加一维,令 d p i , j , k dp_{i,j,k} d p i , j , k 表示前 i i i 种物品,容量为 j j j 的背包第 k k k 大价值。转移时,要求合并 d p i − 1 , j dp_{i-1,j} d p i − 1 , j 与 d p i − 1 , j − w i + v i dp_{i-1,j-w_i}+v_i d p i − 1 , j − w i + v i 两个递减序列,取前 k k k 大值。这里可以使用某些数据结构或双指针法。这样做时间 O ( n m k ) O(nmk) O ( nmk ) ,空间 O ( m k ) O(mk) O ( mk ) 。
完全背包
每种物品有无数个。
仍然设 d p i , j dp_{i, j} d p i , j 表示前 i i i 种物品,容量为 j j j 的背包最大价值。
则
d p i , j = max ( d p i − 1 , j , d p i , j − w i + v i ) dp_{i, j}=\max(dp_{i-1,j}, dp_{i,j-w_i}+v_i)
d p i , j = max ( d p i − 1 , j , d p i , j − w i + v i )
这个转移成立的理由是,d p i , j − w i dp_{i,j-w_i} d p i , j − w i 已经被 d p i , j − k w i dp_{i,j-kw_i} d p i , j − k w i 所更新过,其中 k k k 是不小于 2 2 2 的整数。因此,我们取 max \max max 的时候已经考虑了当前物品取 [ 1 , k ] [1,k] [ 1 , k ] 个的情况。
继续滚动数组优化。注意到将 01 背包的内层循环改为正向枚举,即可让 d p j dp_{j} d p j 被 d p j − k w i dp_{j-kw_i} d p j − k w i 更新。
仍然时间 O ( n m ) O(nm) O ( nm ) ,空间 O ( m ) O(m) O ( m ) 。
板子 +1,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 const int N = 1e7 + 5 ;long long n, m, w[N], v[N], dp[N];void _main() { cin >> m >> n; for (int i = 1 ; i <= n; i++) cin >> w[i] >> v[i]; for (int i = 1 ; i <= n; i++) { for (int j = w[i]; j <= m; j++) dp[j] = max (dp[j], dp[j - w[i]] + v[i]); } cout << dp[m]; }
比较裸的多测完全背包。注意到 a a a 是 1000 1000 1000 的倍数,可以优化一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const int N = 1e6 + 5 ;int m, y, n, w[50 ], v[50 ], dp[N];void _main() { cin >> m >> y >> n; for (int i = 1 ; i <= n; i++) cin >> w[i] >> v[i], w[i] /= 1000 ; long long cur = m; while (y--) { memset (dp, 0 , sizeof (dp)); int m1 = cur / 1000 ; for (int i = 1 ; i <= n; i++) { for (int j = w[i]; j <= m1; j++) dp[j] = max (dp[j], dp[j - w[i]] + v[i]); } cur += dp[m1]; } cout << cur; }
多重背包
每种物品有 c i c_i c i 个。
一个朴素的想法是把第 i i i 种物品拆分成 c i c_i c i 种只有一个的物品,跑 01 背包。复杂度为 O ( m ∑ c i ) O(m\sum c_i) O ( m ∑ c i ) ,显然喜提 TLE。
经典的优化是二进制分组。对一个数字 c i c_i c i ,把它按二进制拆成 c i = ∑ 2 d c_i = \sum 2^d c i = ∑ 2 d ,那么若干个 2 d 2^d 2 d 的和可以表示 [ 0 , c i ] [0,c_i] [ 0 , c i ] 的任意数字,证明使用二进制显然。这样做的复杂度为 O ( m ∑ log c i ) O(m \sum \log c_i) O ( m ∑ log c i ) 。
不过这样做近似等于 O ( n m log V ) O(nm \log V) O ( nm log V ) ,有没有什么更快的方法呢?
有的兄弟,有的。回归朴素思想的转移式:
d p i , j = max k = 0 c i ( d p i − 1 , j − k w i + k v i ) dp_{i,j}=\max_{k=0}^{c_i} (dp_{i-1,j-kw_i}+kv_i)
d p i , j = k = 0 max c i ( d p i − 1 , j − k w i + k v i )
注意到,d p i , j dp_{i,j} d p i , j 的有效转移 d p i − 1 , j − k w i dp_{i-1,j-kw_i} d p i − 1 , j − k w i 满足 j ≡ j − k w i ( m o d c i ) j \equiv j-kw_i \pmod {c_i} j ≡ j − k w i ( mod c i ) ,这启示我们把总容量 m m m 拆成模 c i c_i c i 同余类。此时,d p i , j ′ dp_{i,j'} d p i , j ′ 由 d p i , j ′ ′ , j ′ ′ ∈ [ 0 , j ′ ) dp_{i,j''}, j'' \in [0,j') d p i , j ′′ , j ′′ ∈ [ 0 , j ′ ) 转移而来,可以使用单调队列优化。复杂度为 O ( n ∑ c i ∑ m c i ) = O ( n m ) O(n\sum c_i \sum \dfrac{m}{c_i})=O(nm) O ( n ∑ c i ∑ c i m ) = O ( nm ) 。
板子 +2。给出两种方法的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const int N = 1e6 + 5 ;int n, m, w[N], v[N], c[N];int n1, w1[N], v1[N], dp[N]; void _main() { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i] >> w[i] >> c[i]; for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= c[i]; j <<= 1 ) w1[++n1] = w[i] * j, v1[n1] = v[i] * j, c[i] -= j; if (c[i]) w1[++n1] = w[i] * c[i], v1[n1] = v[i] * c[i]; } for (int i = 1 ; i <= n1; i++) { for (int j = m; j >= w1[i]; j--) dp[j] = max (dp[j], dp[j - w1[i]] + v1[i]); } cout << dp[m]; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const int N = 1e6 + 5 ;int n, m, w[N], v[N], c[N];int head, tail, q[N];int dp[N], dp1[N]; void _main() { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i] >> w[i] >> c[i]; for (int i = 1 ; i <= n; i++) { memcpy (dp1, dp, sizeof (dp)); for (int j = 0 ; j < w[i]; j++) { head = tail = 0 ; for (int k = j; k <= m; k += w[i]) { if (head < tail && q[head] < k - w[i] * c[i]) head++; if (head < tail) dp[k] = max (dp1[k], dp1[q[head]] + (k - q[head]) / w[i] * v[i]); while (head < tail && dp1[k] >= dp1[q[tail - 1 ]] + (k - q[tail - 1 ]) / w[i] * v[i]) tail--; q[tail++] = k; } } } cout << dp[m]; }
前面是多重背包板子,后面注意到 m ≤ 5 m \le 5 m ≤ 5 ,直接枚举体积跑 01 背包,两种背包合并答案即可。
这怎么不算混合背包呢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const int N = 1e5 + 5 ;#define int long long int n, m, w[N], v[N], c[N];int n1, w1[N], v1[N], dp[N]; int nn, A[10 ], B[10 ], C[10 ];void _main() { cin >> n >> nn >> m; for (int i = 1 ; i <= n; i++) cin >> w[i] >> v[i] >> c[i]; for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= c[i]; j <<= 1 ) w1[++n1] = w[i] * j, v1[n1] = v[i] * j, c[i] -= j; if (c[i]) w1[++n1] = w[i] * c[i], v1[n1] = v[i] * c[i]; } for (int i = 1 ; i <= n1; i++) { for (int j = m; j >= w1[i]; j--) dp[j] = max (dp[j], dp[j - w1[i]] + v1[i]); } for (int i = 1 ; i <= nn; i++) cin >> A[i] >> B[i] >> C[i]; for (int i = 1 ; i <= nn; i++) { for (int j = m; j >= 1 ; j--) { for (int k = 0 ; k <= j; k++) dp[j] = max (dp[j], dp[j - k] + A[i] * k * k + B[i] * k + C[i]); } } cout << dp[m]; }
这是一道多重背包的可行性问题。事实上,背包可行性问题的判断适用 bitset 优化,且代码更为简单。
用 bitset<N> s 压位记录状态,第 i i i 位表示 i i i 能否取到,那么取 w i w_i w i 的值即为 s << w[i],所以转移为 s |= s << w[i]。
1 2 3 4 5 6 7 8 9 10 11 12 const int N = 1005 ;int a[8 ], w[8 ] = {1 , 2 , 3 , 5 , 10 , 20 }; bitset<N> s;void _main() { for (int i = 0 ; i < 6 ; i++) cin >> a[i]; s[0 ] = 1 ; for (int i = 0 ; i < 6 ; i++) { for (int j = 0 ; j < a[i]; j++) s |= s << w[i]; } cout << "Total=" << s.count () - 1 ; }
分组背包
每种物品属于一个组,每组只能选一种。
思考一下与普通背包的差别,从所有物品中选变成从每组中选,所以对每组跑背包即可。复杂度多乘上一个组数。
板子 +3。更远古的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <bits/stdc++.h> #define N 1005 using namespace std;namespace BackpackGroup { int s, n, w[N], v[N], g[N], cnt[N * N], f[N][N], dp[N]; int backpack () { int y = *max_element (g + 1 , g + n + 1 ); for (int i = 1 ; i <= n; i++) f[g[i]][++cnt[g[i]]] = i; for (int k = 1 ; k <= y; k++) { for (int j = s; j >= 0 ; j--) { for (int i = 1 ; i <= cnt[k]; i++) { int idx = f[k][i]; if (j >= w[idx]) dp[j] = max (dp[j], dp[j - w[idx]] + v[idx]); } } } return dp[s]; } } using namespace BackpackGroup; signed main () { ios::sync_with_stdio (false ); cin.tie (nullptr ), cout.tie (nullptr ); cin >> s >> n; for (int i = 1 ; i <= n; i++) cin >> w[i] >> v[i] >> g[i]; cout << backpack (); return 0 ; }
这题真正难的地方不是背包,而是转化。
把城堡看作物品,兵力看作体积,转换为背包模型。贪心地,对于每个城堡,若兵力 x > y x>y x > y 且能打赢 x x x ,那就一定能打赢 y y y 。于是将输入的数组转置并排序。这时我们发现这已经被转化为分组背包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const int N = 105 ;int s, n, m, a[N][N], dp[20005 ];void _main() { cin >> s >> n >> m; for (int i = 1 ; i <= s; i++) { for (int j = 1 ; j <= n; j++) cin >> a[j][i]; } for (int i = 1 ; i <= n; i++) sort (a[i] + 1 , a[i] + s + 1 ); for (int i = 1 ; i <= n; i++) { for (int j = m; j >= 0 ; j--) { for (int k = 1 ; k <= s; k++) { dp[j] = max (dp[j], j - 2 * a[i][k] - 1 < 0 ? 0 : dp[j - 2 * a[i][k] - 1 ] + k * i); } } } cout << dp[m]; }