本章基于《背包九讲》展开
背包问题是线性DP中一类重要而特殊的模型。背包问题,不是线性DP的全部,更不是DP的全部
注意,优化的过程,需要认真思考,认识其本质
本章需要细细琢磨,慢慢理解,深刻理解


0/1背包问题

image.png
image.png
image.png
image.png
image.png

例题,01背包问题(含优化)

image.png

  1. #include <iostream>
  2. #include <algorithm>
  3. using namespace std;
  4. const int N = 1010;
  5. int n, m;
  6. int v[N], w[N];
  7. int f[N][N];
  8. int main()
  9. {
  10. cin >> n >> m;
  11. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
  12. for (int i = 1; i <= n; i++)
  13. for (int j = 0; j <= m; j++)
  14. {
  15. f[i][j] = f[i - 1][j]; // 右边集合为空,不包含i
  16. if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
  17. }
  18. cout << f[n][m] << endl;
  19. return 0;
  20. }
  1. // 滚动数组优化,f[N][M] -> f[2][M]
  2. #include <iostream>
  3. using namespace std;
  4. const int N = 1010;
  5. int n, m;
  6. int v[N], w[N];
  7. int f[2][N];
  8. //计算f[i]这一层,只用到了f[i-1]这一层的数据,用滚动数组
  9. int main()
  10. {
  11. cin >> n >> m;
  12. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
  13. f[0][0] = 0;
  14. for (int i= 1; i <= n; i++)
  15. for (int j = 0; j <= m; j++)
  16. {
  17. f[i & 1][j] = f[(i - 1) & 1][j];
  18. if (j >= v[i]) f[i & 1][j] = max(f[i & 1][j], f[(i - 1) & 1][j - v[i]] + w[i]);
  19. }
  20. cout << f[n & 1][m] << endl;
  21. return 0;
  22. }
  1. // 二维优化到一维
  2. #include <iostream>
  3. #include <algorithm>
  4. using namespace std;
  5. const int N = 1010;
  6. int n, m;
  7. int v[N], w[N];
  8. int f[N];
  9. int main()
  10. {
  11. cin >> n >> m;
  12. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
  13. for (int i = 1; i <= n; i++)
  14. for (int j = m; j >= v[i]; j--)
  15. {
  16. f[j] = max(f[j], f[j - v[i]] + w[i]);
  17. //这与我们的刚才的二维里的状态计算不一致,刚才实际应该是f[i - 1][j - v[i]]
  18. //若j从大到小枚举,算f[j]的时候,f[j - v[i]]还没有被更新过,存的就是f[i - 1][j - v[i]]
  19. //这样就和二维的状态转移方程等价了
  20. }
  21. cout << f[m] << endl;
  22. return 0;
  23. }

image.png
image.png

  1. // define ZerOnePack
  2. #include <iostream>
  3. using namespace std;
  4. const int N = 1010;
  5. int n, m;
  6. int f[N];
  7. int v[N], w[N];
  8. void zeroonepack(int f[], int vi, int wi)
  9. {
  10. for (int j = m; j >= vi; j--)
  11. f[j] = max(f[j], f[j - vi] + wi);
  12. }
  13. int main()
  14. {
  15. cin >> n >> m;
  16. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
  17. for (int i = 1; i <= n; i++)
  18. zeroonepack(f, v[i], w[i]);
  19. cout << f[m] << endl;
  20. return 0;
  21. }

初始化的细节问题

image.png
image.png
image.png

完全背包问题

image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png

image.png

例题,完全背包问题(含优化)

image.png

  1. // 3 loops
  2. #include <iostream>
  3. #include <algorithm>
  4. using namespace std;
  5. const int N = 1010;
  6. int n, m;
  7. int v[N], w[N];
  8. int f[N][N];
  9. int main()
  10. {
  11. cin >> n >> m;
  12. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
  13. /*
  14. for 物品 枚举阶段
  15. for 体积 枚举状态
  16. for 决策 枚举决策
  17. */
  18. for (int i = 1; i <= n; i++)
  19. for (int j = 0; j <= m; j++)
  20. for (int k = 0; k *v[i] <= j; k++)
  21. f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
  22. cout << f[n][m] << endl;
  23. return 0;
  24. }

image.png

  1. // 根据以上对转移方程的数学分析
  2. // 3 loops -> 2 loops,减少了一层k循环
  3. #include <iostream>
  4. #include <algorithm>
  5. using namespace std;
  6. const int N = 1010;
  7. int n, m;
  8. int v[N], w[N];
  9. int f[N][N];
  10. int main()
  11. {
  12. cin >> n >> m;
  13. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
  14. for (int i = 1; i <= n; i++)
  15. for (int j = 0; j <= m; j++)
  16. {
  17. f[i][j] = f[i - 1][j];
  18. if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
  19. }
  20. cout << f[n][m] << endl;
  21. return 0;
  22. }
  1. # 二维变成一维
  2. f[j]的时候,f[jv[i]]已经被算过了,
  3. f[j-vi]是第i层的 f[j-vi] 和状态转移方程一致
  1. // f[i][j] -> f[j]
  2. // 二维变一维
  3. #include <iostream>
  4. #include <algorithm>
  5. using namespace std;
  6. const int N = 1010;
  7. int n, m;
  8. int v[N], w[N];
  9. int f[N];
  10. int main()
  11. {
  12. cin >> n >> m;
  13. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
  14. for (int i = 1; i <= n; i++)
  15. for (int j = v[i]; j <= m; j++)
  16. f[j] = max(f[j], f[j - v[i]] + w[i]);
  17. cout << f[m] << endl;
  18. return 0;
  19. }

想清楚: 一维的写法,为什么只差了从大到小循环 和 从小大到循环

image.png

  1. // define completepack
  2. #include <iostream>
  3. using namespace std;
  4. const int N = 1010;
  5. int n, m;
  6. int v[N], w[N];
  7. int f[N];
  8. void completepack(int f[], int vi, int wi)
  9. {
  10. for (int j = vi; j <= m; j++)
  11. f[j] = max(f[j], f[j - vi] + wi);
  12. }
  13. int main()
  14. {
  15. cin >> n >> m;
  16. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
  17. for (int i = 1; i <= n; i++)
  18. completepack(f, v[i], w[i]);
  19. cout << f[m] << endl;
  20. return 0;
  21. }

多重背包问题

image.png
image.png
image.png
image.png
image.png

例题,多重背包问题 I

image.png
image.png

  1. // 多重背包问题 I
  2. // 0 < N,V ≤ 100
  3. // 0 < vi,wi,si ≤ 100
  4. // 3 loops
  5. // O(n^3)
  6. #include <iostream>
  7. #include <algorithm>
  8. using namespace std;
  9. const int N = 110;
  10. int n, m;
  11. int v[N], w[N], s[N];
  12. int f[N][N];
  13. int main()
  14. {
  15. cin >> n >> m;
  16. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
  17. for (int i = 1; i <= n; i++)
  18. for (int j = 0; j <= m; j++)
  19. for (int k = 0; k <= s[i] && k * v[i] <= j; k++)
  20. f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
  21. cout << f[n][m] << endl;
  22. return 0;
  23. }
  1. // 另一种很直接的方法是
  2. // 把第i种物品看做独立的Ci个物品,转化为∑Ci个物品的0/1背包问题
  3. // https://www.acwing.com/problem/content/4/
  4. #include <bits/stdc++.h>
  5. using namespace std;
  6. const int N = 110;
  7. int dp[N];
  8. int v[N], w[N], s[N], n, V;
  9. int main(){
  10. cin >> n >> V;
  11. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
  12. for (int i = 1; i <= n; i++)
  13. for (int j = 1; j <= s[i]; j++)
  14. for (int k = V; k >= v[i]; k--)
  15. dp[k] = max(dp[k], dp[k - v[i]] + w[i]);
  16. cout << dp[V] << '\n';
  17. return 0;
  18. }

例题,多重背包问题 II,倍增

  1. // 多重背包问题 II
  2. // 0 < N ≤ 1000
  3. // 0 < V ≤ 2000
  4. // 0 < vi,wi,si ≤ 2000
  5. // 3loops 会TLE,这里用到二进制优化(倍增思想),转化成01背包问题

image.png一个数,可以用几个动态规划 第3讲 背包九讲 - 图34表示出来
image.png不是一个数,恰好可以用几个动态规划 第3讲 背包九讲 - 图36表示出来,还会有一点余份,比如10,用124可以表示0-7,多来一个3,就可以表示0-10。(不能用1248来表示,因为就会表示多了,11-15也可以表示了)
image.png
image.png

  1. // 多重背包问题 II
  2. // O(Nlogs * V)
  3. // 化简为O(NVlogs)
  4. #include <iostream>
  5. #include <algorithm>
  6. using namespace std;
  7. const int N = 25000, M = 2010;
  8. int n, m;
  9. int v[N], w[N];
  10. int f[N];
  11. int main()
  12. {
  13. cin >> n >> m;
  14. int cnt = 0;
  15. for (int i = 1; i <= n; i++)
  16. {
  17. int a, b, s;
  18. cin >> a >> b >> s;
  19. int k = 1;
  20. while (k <= s)
  21. {
  22. cnt++;
  23. v[cnt] = a * k;
  24. w[cnt] = b * k;
  25. s -= k;
  26. k *= 2;
  27. }
  28. if (s > 0)
  29. {
  30. cnt++;
  31. v[cnt] = a * s;
  32. w[cnt] = b * s;
  33. }
  34. }
  35. n = cnt;
  36. for (int i = 1; i <= n; i++) // 这个n是 原n*logs
  37. for (int j = m; j >= v[i]; j--)
  38. f[j] = max(f[j], f[j - v[i]] + w[i]);
  39. cout << f[m] << endl;
  40. return 0;
  41. }
  1. // define multiplepack
  2. #include <iostream>
  3. using namespace std;
  4. const int N = 1010, M = 2010;
  5. int n, m;
  6. int v[N], w[N], s[N];
  7. int f[M];
  8. void zeroonepack(int f[], int vi, int wi)
  9. {
  10. for (int j = m; j >= vi; j--)
  11. f[j] = max(f[j], f[j - vi] + wi);
  12. }
  13. void completepack(int f[], int vi, int wi)
  14. {
  15. for (int j = vi; j <= m; j++)
  16. f[j] = max(f[j], f[j - vi] + wi);
  17. }
  18. void multiplepack(int f[], int vi, int wi, int si)
  19. {
  20. if (vi * si >= m){
  21. completepack(f, vi, wi);
  22. return ;
  23. }
  24. int k = 1;
  25. while (k < si){
  26. zeroonepack(f, k*vi, k*wi);
  27. si -= k;
  28. k *= 2;
  29. }
  30. if (si){
  31. zeroonepack(f, si*vi, si*wi);
  32. }
  33. }
  34. int main()
  35. {
  36. cin >> n >> m;
  37. for (int i = 1; i <= n; i++){
  38. cin >> v[i] >> w[i] >> s[i];
  39. multiplepack(f, v[i], w[i], s[i]);
  40. }
  41. cout << f[m] << endl;
  42. return 0;
  43. }

例题,多重背包问题 III,单调队列优化

  1. // 多重背包问题 III
  2. // 0<N≤1000
  3. // 0<V≤20000
  4. // 0<vi,wi,si≤20000
  5. // O(NVlogs)也会爆缸
  6. // 需要使用单调队列优化方法
  7. // 单调队列优化,O(NV)
  1. 多重背包问题 I
  2. for(int i=1;i<=n;i++)
  3. for(int j=0;j<=m;j++)
  4. for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
  5. f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
  6. 即枚举到第i个物品时,要将f[0~m]的状态全部都更新一遍.
  7. 状态转移方程为:
  8. f[i][j]=max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,...,f[i-1][j-sv]+sw);
  9. 这样的时间复杂度过大
  10. f[i][j]=max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,...,f[i-1][j-sv]+sw);
  11. f[i][j-v]=max(f[i-1][j-v],f[i-1][j-2v]+w,f[i-1][j-3v]+2w,...,f[i-1][j-(s+1)v]+sw);
  12. f[i][j-v]+w=max(f[i-1][j-v]+w,f[i-1][j-2v]+2w,...,f[i-1][j-sv]+sw,f[i-1][j-(s+1)]+(s+1)w);
  13. 无法像完全背包一样进行优化
  14. 完全背包 是一口气把所有体积全部用掉,即
  15. max(a,b,c,d)=max(a,max(b,c,d))
  16. 多重背包 对于每个物品的个数是有限制的,导致我们最终的等式是如下样子:
  17. max(a,b,c,d)≠max(a,max(b,c,d,e))
  18. 不能像完全背包那样进行推式子,进行优化
  1. 我们可以把这个式子 继续 推导下去,直到背包体积被用到不能再用为止

image.png

  1. 其中 r = j mod vi
  2. 也可以理解为 完全背包 下把当前物品 选到不能再选 后,剩下的 余数
  3. 得到 f(i,r)=f(i1,r)后,我们再利用 完全背包优化思路 往回倒推一遍
  4. 会惊奇的发现一个 滑动窗口求最大值 的模型
  5. 为了方便观察,我们把 f(i1,j)改写成 fj

image.png

  1. 为了方便观察
  2. 去掉 w,然后把数组展开成一条链

image.png

  1. 于是通过该滑动窗口 ,我们就能在线性的时间里求出
  2. i 阶段里,所有满足 j r mod v f(i,j)
  3. 滑动窗口 求最大值的实现,只需利用队列在队头维护一个最大值的单调递减的单调队列即可
  4. 为了更新所有 i 阶段里的状态 f(i,j),我们只需再额外枚举所有的余数r即可
  1. 滑动窗口内部比较最大值的时候,有一个在之前为了方便观察,被删掉的偏移量 w
  2. 要记得加上再比较
  3. 具体就是 当前下标 和该 最大值的下标 之间差了 x v,那么就要加上 x w
  1. int f[N], g[N]; // f[]存储的是第i层,g[]存储第i-1层,
  2. int q[N]; // q[]存储的是f、g数组中的下标(体积);

image.png

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. const int N = 2e4 + 10;
  4. int f[N], g[N]; // f[]存储的是第i层,g[]存储第i-1层,
  5. int q[N]; // q[]存储的是f、g数组中的下标(体积);
  6. int main()
  7. {
  8. int n, m;
  9. scanf("%d%d", &n, &m);
  10. for(int i = 1; i <= n; i++){
  11. int v, w, s;
  12. scanf("%d%d%d", &v, &w, &s);
  13. memcpy(g, f, sizeof g); // 备份上一层的结果
  14. for(int r = 0; r < v; r++){ // 枚举余数
  15. int tt = -1, hh = 0; // tt代表队尾,hh代表对头(最前面的元素)
  16. // 在余数固定的基础上枚举可以用的值
  17. // j枚举体积,单调队列模板
  18. for(int j = r; j <= m; j += v){
  19. while(hh <= tt && q[hh] < j-s*v) hh++;
  20. // 判断是否超出了s件物品;
  21. if (hh <= tt) f[j] = max(f[j], g[q[hh]] + (j - q[hh]) / v * w);
  22. // max(f[i-1][j], f[i-1][能转移里最大] + 个数*wi)
  23. // q[hh]存的是最大值对应的下标是多少, 就是体积
  24. // g[q[hh]]存的就是这个体积能代表的最大价值
  25. // 窗口内的最大值 + 收益(放了多少个物品)
  26. while(hh <= tt && g[q[tt]] - (q[tt] - r) / v * w <= g[j] - (j - r) / v * w) tt--;
  27. // 维护单调性
  28. // q[tt]存的值刨去 (q[tt]-r)/v*w,小于当前g[j]刨去 (j-r)/v*w
  29. // q[tt]存的值(这个位置),就没有意义了
  30. // (q[tt]-r)/v表示个数,(j-r)/v也表示个数
  31. q[++tt] = j;
  32. }
  33. }
  34. }
  35. cout << f[m] << endl;//输出最终结果
  36. return 0;
  37. }

image.png

  1. while(hh <= tt && g[q[tt]] - (q[tt] - r) / v * w <= g[j] - (j - r) / v * w) tt--;
  2. // 每次入队的值是 g[j+k*v] - k*w,对这段代码的进一步解释
  3. m 一定等于 k*v + r,其中 0 <= r < v
  4. 所以,我们可以把 dp 数组分成 r 个类,每一类中的值,都是在同类之间转换得到的
  5. 这些r个类的并集就是0~m
  6. 问题就变成了r个单调队列的问题
  7. 所以,我们可以得到
  8. g[r] = g[r]
  9. g[r+v] = max(g[r] + w, g[r+v])
  10. g[r+2v] = max(g[r] + 2w, g[r+v] + w, g[r+2v])
  11. g[r+3v] = max(g[r] + 3w, g[r+v] + 2w, g[r+2v] + w, g[r+3v])
  12. ...
  13. 但是,这个队列中前面的数,每次都会增加一个 w ,所以我们需要做一些转换
  14. g[r] = g[r]
  15. g[r+v] = max(g[r], g[r+v] - w) + w
  16. g[r+2v] = max(g[r], g[r+v] - w, g[r+2v] - 2w) + 2w
  17. g[r+3v] = max(g[r], g[r+v] - w, g[r+2v] - 2w, g[r+3v] - 3w) + 3w
  18. ...
  19. 这样,每次入队的值是 g[r+k*v] - k*w
  20. 所以,我们在单调队列更新队尾的时候,进行的比较是
  21. g[q[tt]] - (q[tt] - r) / v * w <= g[j] - (j - r) / v * w
  1. // 无注释版本
  2. #include <bits/stdc++.h>
  3. using namespace std;
  4. const int N = 2e4 + 10;
  5. int f[N], g[N]; // f[]存储的是第i层,g[]存储第i-1层,
  6. int q[N]; // q[]存储的是f、g数组中的下标(体积);
  7. int main()
  8. {
  9. int n, m;
  10. scanf("%d%d", &n, &m);
  11. for(int i = 1; i <= n; i++){
  12. int v, w, s;
  13. scanf("%d%d%d", &v, &w, &s);
  14. memcpy(g, f, sizeof g);
  15. for(int r = 0; r < v; r++){ // 枚举余数
  16. int tt = -1, hh = 0;
  17. for(int j = r; j <= m; j += v){
  18. while(hh <= tt && q[hh] < j-s*v) hh++;
  19. if (hh <= tt) f[j] = max(f[j], g[q[hh]] + (j - q[hh]) / v * w);
  20. while(hh <= tt && g[q[tt]] - (q[tt] - r) / v * w <= g[j] - (j - r) / v * w) tt--;
  21. q[++tt] = j;
  22. }
  23. }
  24. }
  25. cout << f[m] << endl;
  26. return 0;
  27. }

混合三种背包问题

image.png

image.png
image.png
image.png
01背包

  1. def ZeroOnePack(F, C, W )
  2. for v = V to C
  3. F[v] = max(F[v],f[v C] + W)
  4. for i = 1 to N
  5. ZeroOnePack(F, Ci, Wi)

完全背包

  1. def CompletePack(F, C, W )
  2. for v = C to V
  3. F[v] = max{F[v],f[v C] + W}

多重背包

  1. def MultiplePack(F, C, W, M)
  2. if C·M V
  3. CompletePack(F, C, W)
  4. return
  5. k := 1
  6. while k < M
  7. ZeroOnePack(kC, kW) M := M k
  8. k := 2k
  9. ZeroOnePack(C·M, W·M)

01背包+完全背包的混合

  1. for i = 1 to N
  2. if i件物品属于01背包
  3. for v = V to Ci
  4. F[v] = max(F[v],F[v Ci] + Wi)
  5. else if i件物品属于完全背包
  6. for v = Ci to V
  7. F[v] = max(F[v],F[v Ci] + Wi)

01背包+完全背包+多重背包的混合

  1. for i = 1 to N
  2. if i件物品属于01背包
  3. ZeroOnePack(F ,Ci ,Wi )
  4. else if i件物品属于完全背包
  5. CompletePack(F ,Ci ,Wi )
  6. else if i件物品属于多重背包
  7. MultiplePack(F ,Ci ,Wi ,Ni )

image.png

例题,混合背包问题

  1. #include <cstring>
  2. #include <iostream>
  3. #include <algorithm>
  4. using namespace std;
  5. const int N = 1010;
  6. int n, m;
  7. int f[N];
  8. int main()
  9. {
  10. cin >> n >> m;
  11. for (int i = 0; i < n; i++)
  12. {
  13. int v, w, s;
  14. cin >> v >> w >> s;
  15. if (s == 0) //完全背包
  16. {
  17. for (int j = v; j <= m; j++) f[j] = max(f[j], f[j - v] + w);
  18. }
  19. else
  20. {
  21. if (s == -1) s = 1;
  22. for (int k = 1; k <= s; k *= 2)
  23. {
  24. for (int j = m; j >= k * v; j--)
  25. f[j] = max(f[j], f[j - k * v] + k * w);
  26. s -= k;
  27. }
  28. if (s)
  29. {
  30. for (int j = m; j >= s * v; j--)
  31. f[j] = max(f[j], f[j - s * v] + s * w);
  32. }
  33. }
  34. }
  35. cout << f[m] << endl;
  36. return 0;
  37. }
  38. // 也可以存起来,再来一遍
  39. #include <bits/stdc++.h>
  40. using namespace std;
  41. const int N = 40, M = 220, MM = 1e5 + 10;
  42. int dp[N][M], w[N], v[N], s[N];
  43. int V, n;
  44. int f[MM], nw[MM], nv[MM], tot, knap[MM];
  45. int main()
  46. {
  47. cin >> V >> n;
  48. for (int i = 1; i <= n; i++){
  49. cin >> w[i] >> v[i] >> s[i];
  50. if (s[i] == 0){
  51. nw[++tot] = w[i]; nv[tot] = v[i];
  52. knap[tot] = 1; //完全背包
  53. }
  54. else if (s[i] == 1){ //01背包
  55. nw[++tot] = w[i]; nv[tot] = v[i];
  56. }
  57. else{ //多重背包
  58. for (int j = 1; j <= s[i]; j = j << 1){
  59. s[i] -= j;
  60. nw[++tot] = w[i] * j; nv[tot] = v[i] * j;
  61. }
  62. if (s[i]){
  63. nw[++tot] = w[i] * s[i]; nv[tot] = v[i] * s[i];
  64. s[i] = 0;
  65. }
  66. }
  67. }
  68. for (int i = 1; i <= tot; i++){
  69. if (knap[i]){
  70. for (int j = nw[i]; j <= V; j++)
  71. f[j] = max(f[j], f[j - nw[i]] + nv[i]);
  72. }
  73. else{
  74. for (int j = V; j >= nw[i]; j--)
  75. f[j] = max(f[j], f[j - nw[i]] + nv[i]);
  76. }
  77. }
  78. cout << f[V] << '\n';
  79. return 0;
  80. }
  1. // define zeroonepack(),completepack(),multiplepack()
  2. #include <iostream>
  3. using namespace std;
  4. const int N = 1010;
  5. int n, m;
  6. int v[N], w[N], s[N];
  7. int f[N];
  8. void zeroonepack(int f[], int vi, int wi)
  9. {
  10. for (int j = m; j >= vi; j--)
  11. f[j] = max(f[j], f[j - vi] + wi);
  12. }
  13. void completepack(int f[], int vi, int wi)
  14. {
  15. for (int j = vi; j <= m; j++)
  16. f[j] = max(f[j], f[j - vi] + wi);
  17. }
  18. void multiplepack(int f[], int vi, int wi, int si)
  19. {
  20. if (vi * si >= m){
  21. completepack(f, vi, wi);
  22. return ;
  23. }
  24. int k = 1;
  25. while (k < si){
  26. zeroonepack(f, k*vi, k*wi);
  27. si -= k;
  28. k *= 2;
  29. }
  30. if (si){
  31. zeroonepack(f, si*vi, si*wi);
  32. }
  33. }
  34. int main()
  35. {
  36. cin >> n >> m;
  37. for (int i = 1; i <= n; i++){
  38. cin >> v[i] >> w[i] >> s[i];
  39. if (s[i] == -1) zeroonepack(f, v[i], w[i]);
  40. else if (s[i] == 0) completepack(f, v[i], w[i]);
  41. else multiplepack(f, v[i], w[i], s[i]);
  42. }
  43. cout << f[m] << endl;
  44. return 0;
  45. }

二维费用背包

image.png
image.png
image.png

例题,二维费用的背包问题

image.png

  1. #include <iostream>
  2. using namespace std;
  3. const int N = 110;
  4. int n, V, M;
  5. int f[N][N];
  6. int main()
  7. {
  8. cin >> n >> V >> M;
  9. for (int i = 0; i < n; i++)
  10. {
  11. int v, m, w;
  12. cin >> v >> m >> w;
  13. for (int j = V; j >= v; j--)
  14. for (int k = M; k >= m; k--)
  15. f[j][k] = max(f[j][k], f[j - v][k - m] + w);
  16. }
  17. cout << f[V][M] << endl;
  18. return 0;
  19. }

例题,1271:【例9.15】潜水员

image.png
image.png

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. const int N = 1010;
  4. int dp[N][25][90];
  5. int v1[N], v2[N], w[N];
  6. int n, m, s;
  7. int main()
  8. {
  9. cin >> n >> m >> s;
  10. for (int i = 1; i <= s; i++)
  11. cin >> v1[i] >> v2[i] >> w[i];
  12. memset(dp, 0x3f, sizeof dp);
  13. dp[0][0][0] = 0;
  14. for (int i = 1; i <= s; i++)
  15. for (int j1 = 0; j1 <= n; j1++)
  16. for (int j2 = 0; j2 <= m; j2++){
  17. dp[i][j1][j2] = dp[i - 1][j1][j2];
  18. dp[i][j1][j2] = min(dp[i][j1][j2],
  19. dp[i - 1][max(0, j1 - v1[i])][max(0, j2 - v2[i])] + w[i]);
  20. }
  21. cout << dp[s][n][m] << '\n';
  22. return 0;
  23. }

分组背包

image.png
image.png
image.png

例题,分组背包问题

image.png

  1. // 这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件 都不选
  2. // 这里三层循环的顺序保证了每一组内的物品最多只有一个会被添加到背包中
  3. // 分组的背包问题将彼此互斥的若干物品称为一个组,这建立了一个很好的模型
  4. // 不少背包问题的变形都可以转化为分组的背包问题
  5. // 由分组的背包问题进一步可定义“泛化物品”的概念,十分有利于解题
  6. #include <iostream>
  7. #include <algorithm>
  8. using namespace std;
  9. const int N = 110;
  10. int n, m;
  11. int v[N][N], w[N][N], s[N];
  12. int f[N];
  13. int main()
  14. {
  15. cin >> n >> m;
  16. for (int i = 1; i <= n; i++)
  17. {
  18. cin >> s[i];
  19. for (int j = 0; j < s[i]; j++)
  20. cin >> v[i][j] >> w[i][j];
  21. }
  22. for (int i = 1; i <= n; i++)
  23. for (int j = m; j >= 0; j--)
  24. for (int k = 0; k < s[i]; k++)
  25. if (v[i][k] <= j)
  26. f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
  27. cout << f[m] << endl;
  28. return 0;
  29. }

例题,1272:【例9.16】分组背包

  1. for (int i = 1; i <= T; i++)
  2. for (int j = v; j >= 0; j--)
  3. for (int k = 0; k < (int)h[i].size(); k++)
  4. if (j >= h[i][k].first)
  5. dp[j] = max(dp[j], dp[j - h[i][k].first] + h[i][k].second);

有依赖的背包问题

image.pngimage.png
image.png
image.png

image.png

例题,有依赖的背包问题

image.png
image.png
image.png
image.png

  1. // 这种背包问题的物品间存在某种“依赖”的关系
  2. // 也就是说,物品i 依赖于 物品j,表示若选 物品i,则必须选 物品j
  3. // 为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖
  4. // 另外,没有某件物品同时依赖多件物品。
  5. // 需要会树的存储
  6. #include <cstring>
  7. #include <iostream>
  8. #include <algorithm>
  9. using namespace std;
  10. const int N = 110;
  11. int n, m;
  12. int v[N], w[N];
  13. int h[N], e[N], ne[N], idx;
  14. int f[N][N]; //f[u][j] 所有从以u为根的子树中选,且总体积不超过j的方案
  15. void add(int a, int b)
  16. {
  17. e[idx] = b, ne[idx] = h[a], h[a] = idx++;
  18. }
  19. // 框架是树形DP+每个结点递归的那层, 递归的思路
  20. void dfs(int u)
  21. {
  22. for (int i = h[u]; i != -1; i = ne[i]) //循环物品组
  23. {
  24. int son = e[i];
  25. dfs(e[i]);
  26. //分组背包
  27. for (int j = m - v[u]; j >= 0; j--) //循环体积j
  28. for (int k = 0; k <= j; k++) //循环决策k, 体积是...体积是j
  29. f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
  30. }
  31. //将物品u加进去
  32. for (int i = m; i >= v[u]; i--) f[u][i] = f[u][i - v[u]] + w[u];
  33. for (int i = 0; i < v[u]; i++) f[u][i] = 0; // 体积i不够用,就是0价值
  34. }
  35. int main()
  36. {
  37. cin >> n >> m;
  38. memset(h, -1, sizeof h);
  39. int root;
  40. for (int i = 1; i <= n; i++)
  41. {
  42. int p;
  43. cin >> v[i] >> w[i] >> p;
  44. if (p == -1) root = i;
  45. else add(p, i);
  46. }
  47. dfs(root);
  48. cout << f[root][m] << endl;
  49. return 0;
  50. }
  51. //如果按方案来划分,就有2^k个划分,没法存
  52. //以体积来划分,就会极大的优化(闫式DP)
  53. //用不同的体积表示一大类,从而提高了效率
  54. //转化成,分组背包问题
  55. //每个子树看成每个物品组,有m+1个物品,体积是0的子树,体积是1的子树,体积是m的子树
  56. //f[u][j] = max{f[u][j-k] + f[son][k]}
  1. // 这种背包问题的物品间存在某种“依赖”的关系
  2. // 也就是说,物品i 依赖于 物品j,表示若选 物品i,则必须选 物品j
  3. // 为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖
  4. // 另外,没有某件物品同时依赖多件物品。
  5. // 需要会树的存储
  6. #include <bits/stdc++.h>
  7. using namespace std;
  8. const int N = 110;
  9. int n, m;
  10. int v[N], w[N];
  11. vector<int> g[N];
  12. int f[N][N]; //f[u][j] 所有从以u为根的子树中选,且总体积不超过j的方案
  13. void add(int a, int b)
  14. {
  15. g[a].push_back(b);
  16. }
  17. // 框架是树形DP+每个结点递归的那层, 递归的思路
  18. void dfs(int u)
  19. {
  20. for (auto son : g[u]) //循环物品组
  21. {
  22. dfs(son);
  23. //分组背包
  24. for (int j = m - v[u]; j >= 0; j--) //循环体积j
  25. for (int k = 0; k <= j; k++) //循环决策k, 体积是...体积是j
  26. f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
  27. }
  28. //将物品u加进去
  29. for (int i = m; i >= v[u]; i--) f[u][i] = f[u][i - v[u]] + w[u];
  30. for (int i = 0; i < v[u]; i++) f[u][i] = 0; // 体积i不够用,就是0价值
  31. }
  32. int main()
  33. {
  34. cin >> n >> m;
  35. int root;
  36. for (int i = 1; i <= n; i++)
  37. {
  38. int p;
  39. cin >> v[i] >> w[i] >> p;
  40. if (p == -1) root = i;
  41. else add(p, i);
  42. }
  43. dfs(root);
  44. cout << f[root][m] << endl;
  45. return 0;
  46. }
  47. //如果按方案来划分,就有2^k个划分,没法存
  48. //以体积来划分,就会极大的优化(闫式DP)
  49. //用不同的体积表示一大类,从而提高了效率
  50. //转化成,分组背包问题
  51. //每个子树看成每个物品组,有m+1个物品,体积是0的子树,体积是1的子树,体积是m的子树
  52. //f[u][j] = max{f[u][j-k] + f[son][k]}

背包问题问法的变化

image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png

例题,背包问题求方案数

  1. #include <cstring>
  2. #include <iostream>
  3. using namespace std;
  4. const int N = 1010, mod = 1e9 + 7;
  5. int n, m;
  6. int f[N], g[N];
  7. int main()
  8. {
  9. cin >> n >> m;
  10. memset(f, -0x3f, sizeof f);
  11. f[0]= 0;
  12. g[0] = 1;
  13. for (int i = 0; i < n; i++)
  14. {
  15. int v, w;
  16. cin >> v >> w;
  17. for (int j = m; j >= v; j--)
  18. {
  19. int maxv = max(f[j], f[j - v] + w);
  20. int cnt = 0;
  21. if (maxv == f[j]) cnt += g[j];
  22. if (maxv == f[j - v] + w) cnt += g[j - v];
  23. g[j] = cnt % mod;
  24. f[j] = maxv;
  25. }
  26. }
  27. int res = 0;
  28. for (int i = 0; i <= m; i++) res = max(res, f[i]);
  29. int cnt = 0;
  30. for (int i = 0; i <= m; i++)
  31. if (res == f[i])
  32. cnt = (cnt + g[i]) % mod;
  33. cout << cnt << endl;
  34. return 0;
  35. }

例题,背包问题求具体方案

  1. #include <iostream>
  2. using namespace std;
  3. const int N = 1010;
  4. int n, m;
  5. int v[N], w[N];
  6. int f[N][N];
  7. int main()
  8. {
  9. cin >> n >> m;
  10. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
  11. // 从后往前
  12. for (int i = n; i >= 1; i--){
  13. for (int j = 0; j <= m; j++){ // 并没有进行一维压缩,体积的枚举需要保持从小到大
  14. f[i][j] = f[i + 1][j]; // 无法选择i的决策也要进行转移,把机会留给后面的使用
  15. if (j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
  16. }
  17. }
  18. //f[1][m]是最大价值
  19. int j = m;
  20. for (int i = 1; i <= n; i++)
  21. if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]){
  22. cout << i << ' ';
  23. j -= v[i];
  24. }
  25. puts("");
  26. return 0;
  27. }
  1. // 用递归输出方案,只做到了输出某一个方案,还未实现字典序的问题
  2. #include <iostream>
  3. using namespace std;
  4. const int N = 1010;
  5. int n, m;
  6. int v[N], w[N];
  7. int f[N];
  8. int pre[N];
  9. void print(int j){
  10. if (pre[j] == -1) return ;
  11. print(j - v[pre[j]]);
  12. cout << pre[j] << ' ';
  13. }
  14. int main()
  15. {
  16. cin >> n >> m;
  17. for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
  18. memset(pre, -1, sizeof pre);
  19. for (int i = 1; i <= n; i++)
  20. for (int j = m; j >= v[i]; j--){
  21. if (f[j - v[i]] + w[i] > f[j]){
  22. f[j] = f[j - v[i]] + w[i];
  23. pre[j] = i;
  24. }
  25. }
  26. cout << f[m] << '\n';
  27. //for (int j = 0; j <= m; j++) cout << pre[j] << ' ';
  28. //puts("");
  29. print(m);
  30. puts("");
  31. return 0;
  32. }