当前位置:   article > 正文

动态规划DP——背包问题_背包问题dp算法实现

背包问题dp算法实现

目录

一、01背包问题

1. 题目介绍

2. 题解代码(C++)2.1 版本1 二维

2.2 版本2 一维

2.3 版本3 优化输入

二、完全背包问题

1.三维朴素做法(容易超时)

2.二维做法(优化)​

 3.一维做法(最优化)

三、多重背包问题

四、分组背包问题


一、01背包问题

1. 题目介绍

有 N 件物品和一个容量为 V 的背包,每件物品有各自的价值且只能被选择一次,要求在有限的背包容量下,装入的物品总价值最大。

「0-1 背包」是较为简单的动态规划问题,也是其余背包问题的基础。

动态规划是不断决策求最优解的过程,「0-1 背包」即是不断对第 ii 个物品的做出决策,「0-1」正好代表不选与选两种决定。

2. 题解代码(C++)
2.1 版本1 二维

 (1)状态f[i][j]定义:前 ii 个物品,背包容量 jj 下的最优解(最大价值):

当前的状态依赖于之前的状态,可以理解为从初始状态f[0][0] = 0开始决策,有 NN 件物品,则需要 NN 次决 策,每一次对第 ii 件物品的决策,状态f[i][j]不断由之前的状态更新而来。
(2)当前背包容量不够(j < v[i]),没得选,因此前 ii 个物品最优解即为前 i−1i−1 个物品最优解:

对应代码:f[i][j] = f[i - 1][j]。
(3)当前背包容量够,可以选,因此需要决策选与不选第 ii 个物品:

选:f[i][j] = f[i - 1][j - v[i]] + w[i]。
不选:f[i][j] = f[i - 1][j] 。
我们的决策是如何取到最大价值,因此以上两种情况取 max() 。
代码如下:

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int N=1010;
  4. int v[N]; // 体积
  5. int w[N]; // 价值
  6. int f[N][N]; // f[i][j], j体积下前i个物品的最大价值
  7. int main()
  8. {
  9. int n,m;
  10. cin>>n>>m;
  11. for(int i=1;i<=n;i++)
  12. {
  13. cin>>v[i]>>w[i];
  14. }
  15. for(int i=1;i<=n;i++)
  16. for(int j=1;j<=m;j++)
  17. {
  18. // 当前背包容量装不进第i个物品,则价值等于前i-1个物品
  19. if(j<v[i])
  20. {
  21. f[i][j]=f[i-1][j];
  22. }
  23. // 能装,需进行决策是否选择第i个物品
  24. else
  25. {
  26. f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
  27. }
  28. }
  29. cout << f[n][m] << endl;
  30. }


2.2 版本2 一维

将状态f[i][j]优化到一维f[j],实际上只需要做一个等价变形。

为什么可以这样变形呢?我们定义的状态f[i][j]可以求得任意合法的i与j最优解,但题目只需要求得最终状态f[n][m],因此我们只需要一维的空间来更新状态。

(1)状态f[j]定义:NN 件物品,背包容量j下的最优解。

(2)注意枚举背包容量j必须从m开始。

(3)为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]与f[i - 1][j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。

(4)例如,一维状态第i轮对体积为 33 的物品进行决策,则f[7]由f[4]更新而来,这里的f[4]正确应该是f[i - 1][4],但从小到大枚举j这里的f[4]在第i轮计算却变成了f[i][4]。当逆序枚举背包容量j时,我们求f[7]同样由f[4]更新,但由于是逆序,这里的f[4]还没有在第i轮计算,所以此时实际计算的f[4]仍然是f[i - 1][4]。

(5)简单来说,一维情况正序更新状态f[j]需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。

状态转移方程为:f[j] = max(f[j], f[j - v[i]] + w[i] 。

  1. for(int i = 1; i <= n; i++
  2.     for(int j = m; j >= 0; j--)
  3.     {
  4.         if(j < v[i]) 
  5.             f[i][j] = f[i - 1][j];  // 优化前
  6.             f[j] = f[j];            // 优化后,该行自动成立,可省略。
  7.         else    
  8.             f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);  // 优化前
  9.             f[j] = max(f[j], f[j - v[i]] + w[i]);                   // 优化后
  10.     }    


实际上,只有当枚举的背包容量 >= v[i] 时才会更新状态,因此我们可以修改循环终止条件进一步优化。

  1. for(int i = 1; i <= n; i++)
  2. {
  3.     for(int j = m; j >= v[i]; j--)  
  4.         f[j] = max(f[j], f[j - v[i]] + w[i]);


关于状态f[j]的补充说明
二维下的状态定义f[i][j]是前 ii 件物品,背包容量 jj 下的最大价值。一维下,少了前 ii 件物品这个维度,我们的代码中决策到第 ii 件物品(循环到第i轮),f[j]就是前i轮已经决策的物品且背包容量 jj 下的最大价值。

因此当执行完循环结构后,由于已经决策了所有物品,f[j]就是所有物品背包容量 jj 下的最大价值。即一维f[j]等价于二维f[n][j]。

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int N=1010;
  4. int v[N]; // 体积
  5. int w[N]; // 价值
  6. int f[N]; // f[i][j], j体积下前i个物品的最大价值
  7. int main()
  8. {
  9. int n,m;
  10. cin>>n>>m;
  11. for(int i=1;i<=n;i++)
  12. {
  13. cin>>v[i]>>w[i];
  14. }
  15. for(int i=1;i<=n;i++)
  16. for(int j=m;j>=v[i];j--)
  17. {
  18. // 能装,需进行决策是否选择第i个物品
  19. f[j]=max(f[j],f[j-v[i]]+w[i]);
  20. }
  21. cout << f[m] << endl;
  22. }

2.3 版本3 优化输入


我们注意到在处理数据时,我们是一个物品一个物品,一个一个体积的枚举。

因此我们可以不必开两个数组记录体积和价值,而是边输入边处理。

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int MAXN = 1005;
  4. int f[MAXN];  // 
  5. int main() 
  6. {
  7.     int n, m;   
  8.     cin >> n >> m;
  9.     for(int i = 1; i <= n; i++) {
  10.         int v, w;
  11.         cin >> v >> w;      // 边输入边处理
  12.         for(int j = m; j >= v; j--)
  13.             f[j] = max(f[j], f[j - v] + w);
  14.     }
  15.     cout << f[m] << endl;
  16.     return 0;
  17. }

二、完全背包问题

有 N种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi, wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000

输入样例
4 5
1 2
2 4
3 4
4 5
输出样例
10

1.三维朴素做法(容易超时)

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

2.二维做法(优化)

优化思路
我们列举一下更新次序的内部关系:

  1. f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w ,  f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
  2. f[i , j-v]= max(            f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)


由上两式,可得出如下递推关系: 
                   

     f[i][j]=max(f[i,j-v]+w , f[i-1][j]) 


有了上面的关系,那么其实k循环可以不要了,核心代码优化成这样:

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

 3.一维做法(最优化)

这个代码和01背包的非优化写法很像啊!!!我们对比一下,下面是01背包的核心代码

  1. for(int i = 1 ; i <= n ; i++)
  2. for(int j = 0 ; j <= m ; j ++)
  3. {
  4.     f[i][j] = f[i-1][j];
  5.     if(j-v[i]>=0)
  6.         f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
  7. }

两个代码其实只有一句不同(注意下标)

  1. f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包
  2. f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题

因为和01背包代码很相像,我们很容易想到进一步优化。核心代码可以改成下面这样

  1.  for(int i = 1 ; i<=n ;i++)
  2.     for(int j = v[i] ; j<=m ;j++)//注意了,这里的j是从小到大枚举,和01背包不一样
  3.     {
  4.             f[j] = max(f[j],f[j-v[i]]+w[i]);
  5.     }

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


三、多重背包问题

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N≤10000<N≤1000
0<V≤20000<V≤2000
0<vi,wi,si≤2000

输入样例

  1. 4 5
  2. 1 2 3
  3. 2 4 1
  4. 3 4 3
  5. 4 5 2

输出样例:

10

1.暴力做法(超时)

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

2.二进制优化

  1. #include<iostream>
  2. using namespace std;
  3. const int N = 12010, M = 2010;
  4. int n, m;
  5. int v[N], w[N]; //逐一枚举最大是N*logS
  6. int f[M]; // 体积<M
  7. int main()
  8. {
  9. cin >> n >> m;
  10. int cnt = 0; //分组的组别
  11. for(int i = 1;i <= n;i ++)
  12. {
  13. int a,b,s;
  14. cin >> a >> b >> s;
  15. int k = 1; // 组别里面的个数
  16. while(k<=s)
  17. {
  18. cnt ++ ; //组别先增加
  19. v[cnt] = a * k ; //整体体积
  20. w[cnt] = b * k; // 整体价值
  21. s -= k; // s要减小
  22. k *= 2; // 组别里的个数增加
  23. }
  24. //剩余的一组
  25. if(s>0)
  26. {
  27. cnt ++ ;
  28. v[cnt] = a*s;
  29. w[cnt] = b*s;
  30. }
  31. }
  32. n = cnt ; //枚举次数正式由个数变成组别数
  33. //01背包一维优化
  34. for(int i = 1;i <= n ;i ++)
  35. for(int j = m ;j >= v[i];j --)
  36. f[j] = max(f[j],f[j-v[i]] + w[i]);
  37. cout << f[m] << endl;
  38. return 0;
  39. }

四、分组背包问题

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。

接下来有 N 组数据:

  • 每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
  • 每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤100
0<Si≤100
0<vij,wij≤100

输入样例

  1. 3 5
  2. 2
  3. 1 2
  4. 2 4
  5. 1
  6. 3 4
  7. 1
  8. 4 5

输出样例:

8

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int N=110;
  4. int v[N][N],w[N][N],f[N],s[N];
  5. int main()
  6. {
  7. int n,m;
  8. cin>>n>>m;
  9. for(int i=0;i<n;i++)
  10. {
  11. cin>>s[i];
  12. for(int j=0;j<s[i];j++)
  13. {
  14. cin>>v[i][j]>>w[i][j];
  15. }
  16. }
  17. for(int i=0;i<n;i++)
  18. for(int j=m;j>=0;j--)
  19. for(int k=0;k<s[i];k++)
  20. if(j>=v[i][k])
  21. f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
  22. cout<<f[m];
  23. return 0;
  24. }
本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/article/detail/50819
推荐阅读
相关标签
  

闽ICP备14008679号