动态规划背包问题

动态规划背包问题一、0-1背包1.      有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。(1<=n<=100,1<=wi,vi<=100,1<=W<=10000)样例输入:4231234225 42133457910样例输出…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

一、0-1背包

1.      有n个重量和价值分别为wi, vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。(1<=n<=100,1<=wi, vi<=100,1<=W<=10000)

样例输入:

4

2 3

1 2

3 4

2 2

5

 

4

2 1

3 3

4 5

7 9

10

样例输出:

7

12

【分析一】这是最基础、最著名的背包问题。特点是:每种物品仅有一件,可以选择放或不放。(此问题可引申出各种变种,需灵活掌握)。

        不妨用子问题定义状态:即dp[i][j]表示前i件物品(部分或全部)恰放入一个容量为j的背包时可以获得的最大价值。则状态转移方程:dp[i][j]=max{dp[i-1][j],dp[i-1][j-w[i]]+v[i]}。通过递推完成问题求解。

        这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。

        解释“将前i件物品放入容量为j的背包中”这个子问题:若只考虑第i件物品放/不放,那么该子问题就可以转化为一个只牵扯前i-1件物品的问题

        ① 如果不放第i件物品,问题就转化为“前i-1件物品放入容量为v的背包中”

        ② 如果放第i件物品,问题就转化为“前i-1件物品放入剩下的容量为j-w[i]的背包中”,此时能获得的最大价值就是f [i-1][v-w[i]]再加上通过放入第i件物品获得的价值c[i]。

【分析二】空间复杂度的优化

        以上方法的时间和空间复杂度均为O(nW),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(W)

        先考虑上面的基本思路如何实现:

        肯定是有一个主循环i=1..N,每次算出来二维数组dp[i][0..W]的所有值。那么,如果只用一维数组dp[0..W],能不能保证第i次循环结束后dp[W]中表示的就是我们定义的状态dp[i][j]呢?

        dp[i][j]是由dp[i-1][j]和dp[i-1][j-w[i]]两个子问题递推而来,能否保证在推dp[i][j]时(即在第i次主循环中推dp[j]时)能够得到dp[i-1][j]和dp[i-1][j-w[i]]的值呢?事实上,这要求在每次主循环中我们以j=W..0的逆序推dp[j],这样才能保证推dp[j]时dp[j-w[i]]保存的是状态dp[i-1][j-w[i]]的值(即结果的“无后效性”)。

        代码实现如下:

        for(i=1;i<=N;i++)

                for(j=W;j>=w[i];j–)

                        dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

        其中:dp[j]=max{dp[j], dp[j-w[i]]+v[i]}相当于转移方程dp[i][j]=max{dp[i-1][j],dp[i-1][j-w[i]]+v[i]},因为现在的dp[j-w[i]]就相当于原来的dp[i-1][j-w[i]]。

        然而,如果将j的循环顺序从上面的逆序改成顺序的话,那么则成了dp[i][j]由dp[i][j-w[i]]推知,与本题意不符,但它却是另一个重要的完全背包问题最简捷的解决方案,故只用一维数组解01背包问题是十分必要的(特别是数据规模较大的时候,对空间复杂度的优化十分重要)。

【分析三】多组输入,注意数据的初始化。这里使用memset(dp, 0, sizeof(dp)); 表示开始时背包中未放物品,可获得的最大总价值为0。

法一:二维数组解法

 

 
  1. #include <iostream>

  2. #include <cstdio>

  3. #include <cstring>

  4. using namespace std;

  5. const int maxn=105;

  6. const int maxv=10005;

  7. int N,W;

  8. int w[maxn],v[maxn];

  9. int dp[maxn][maxv]; //dp[i][j]记录前i件物品(部分或全部)放入容量为j的背包中可获得的最大总价值

  10. int main()

  11. {

  12. int i,j;

  13. while(scanf("%d",&N)!=EOF)

  14. {

  15. memset(dp,0,sizeof(dp));

  16. for(i=1;i<=N;i++)

  17. scanf("%d %d",&w[i],&v[i]);

  18. scanf("%d",&W);

  19. for(i=1;i<=N;i++)

  20. {

  21. for(j=W;j>=1;j--)

  22. {

  23. if(j>=w[i])//放第i件物品,有两种情况

  24. dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);

  25. else //不放第i件物品

  26. dp[i][j]=dp[i-1][j];

  27. }

  28. }

  29. printf("%d\n",dp[N][W]);

  30. }

  31. return 0;

  32. }

法二:优化空间复杂度

 
  1. #include <iostream>

  2. #include <cstdio>

  3. #include <cstring>

  4. using namespace std;

  5. const int maxn=105;

  6. const int maxv=10005;

  7. int N,W;

  8. int w[maxn],v[maxn];

  9. int dp[maxv]; //dp[i]记录所选物品总重量不超过i时可获得的最大总价值

  10. int main()

  11. {

  12. int i,j;

  13. while(scanf("%d",&N)!=EOF)

  14. {

  15. memset(dp,0,sizeof(dp));

  16. for(i=1;i<=N;i++)

  17. scanf("%d %d",&w[i],&v[i]);

  18. scanf("%d",&W);

  19. for(i=1;i<=N;i++)

  20. for(j=W;j>=w[i];j--)

  21. dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

  22. printf("%d\n",dp[W]);

  23. }

  24. return 0;

  25. }

2.  0-1背包升级版

        问题情境同上,数据规模:1<=n<=100,1<=wi<=107,1<=vi<=100,1<=W<=109。

【分析】此问题与最初的01背包问题相比,情境相同,只是修改了限制条件的大小。这一次O(nW)的规模就不够用了,不过在这个问题中,相比较重量而言,价值的范围比较小,所以可以试着改变DP的对象。

        定义:dp[i+1][j]为前i个物品中挑选出价值总和为j时总重量的最小值。

        则dp[0][0]=0,dp[0][j]=INF(不存在就赋值为INF)。

        此外,在前i个物品中挑选出价值总和为j时,一定有

                前i-1个物品中挑选价值总和为j的部分【即不选第i个物品】

                前i-1个物品中挑选价值总和为j-v[i]的部分,然后再选中第i个物品【即选第i个物品】

        所以有dp[i+1][j]=min(dp[i][j],dp[i][j-v[i]]+w[i])

        最终的答案就是令dp[n][j]<=W的最大的j,这样复杂度就变为了O(n∑i vi)。在此题的限制条件下便能完成。

 

 
  1. #include <iostream>

  2. #include <cstdio>

  3. #include <cstring>

  4. using namespace std;

  5. const int maxn=105;

  6. const int maxv=105;

  7. const int INF=1000000005;

  8. int n,W;

  9. int w[maxn],v[maxn];

  10. int dp[maxn][maxn*maxv+1]; //dp[i][j]记录前i个物品挑选出价值总和为j时总重量的最小值(不存在时值为无穷大INF)

  11. int main()

  12. {

  13. int i,j;

  14. int ret; //ret记录价值总和的最大值

  15. while(scanf("%d",&n)!=EOF)

  16. {

  17. for(i=0;i<n;i++)

  18. scanf("%d %d",&w[i],&v[i]);

  19. scanf("%d",&W);

  20. for(i=0;i<=maxn*maxv;i++)

  21. dp[0][i]=INF;

  22. dp[0][0]=0; //表示前0个物品中什么都挑选不了,故总重量为0(自然最小值也为0)

  23. for(i=0;i<n;i++)

  24. {

  25. for(j=0;j<=maxn*maxv;j++)

  26. {

  27. if(j<v[i]) //第i件物品的价值比价值总和j还大,则不能选第i件物品

  28. dp[i+1][j]=dp[i][j];

  29. else //否则可以选也可以不选第i件物品

  30. dp[i+1][j]=min(dp[i][j],dp[i][j-v[i]]+w[i]);

  31. }

  32. }

  33. ret=0;

  34. for(i=0;i<=maxn*maxv;i++) //扫描价值总和,找解

  35. {

  36. if(dp[n][i]<=W) //n件物品中挑选出的物品价值总和为i时,总重量<=W,则更新ret

  37. ret=i;

  38. }

  39. printf("%d\n",ret);

  40. }

  41. return 0;

  42. }

二、完全背包

        有n种重量和价值分别为wi和vi的物品。从这些物品中挑选总重量不超过W的物品,求出挑选物品价值总和的最大值。在这里,每种物品可以挑选任意多件。

样例输入:

3

3 4

4 5

2 3

7

 

4

2 1

3 3

4 5

7 9

10

样例输出:

10

12

【分析】这个问题非常类似于上面的0-1背包问题,所不同的是每种物品有任意多件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解0-1背包时的思路,令f[i][j]表示前i种物品恰放入一个容量为j的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:f[i][j]=max{f[i-1][j-k*w[i]]+k*c[i]|0<=k*w[i]<= j}。

  将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明0-1背包问题的方程的确是很重要,可以推及其它类型的背包问题。

        这个算法使用一维数组,代码如下:

        for(i=1;i<=N;i++)

                for(j=w[i];j<=W;j++)

                        dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

  你会发现,这个伪代码与01背包问题的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么0-1背包问题中要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-w[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-w[i]]。而现在完全背包的特点恰是每种物品可选任意多件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-w[i]],所以就可以并且必须采用v= 0..V的顺序循环。这就是这个简单的程序为何成立的道理。

  这个算法也可以以另外的思路得出。例如,基本思路中的状态转移方程可以等价地变形成这种形式:f[i][v]=max{f[i-1][v],f[i][v-w[i]]+c[i]},将这个方程用一维数组实现,便得到了上面的伪代码。

法一:

 
  1. #include <iostream>

  2. #include <cstdio>

  3. #include <cstring>

  4. using namespace std;

  5. int n,W;

  6. int w[105],v[105];

  7. int dp[105][10005]; //dp[i][j]记录从前i种物品选一些,放入容量为j的背包中可获得的最大总价值

  8. int main()

  9. {

  10. int i,j;

  11. while(scanf("%d",&n)!=EOF)

  12. {

  13. for(i=1;i<=n;i++)

  14. scanf("%d %d",&w[i],&v[i]);

  15. scanf("%d",&W);

  16. memset(dp,0,sizeof(dp));

  17. for(i=1;i<=n;i++)

  18. {

  19. for(j=1;j<=W;j++) //注意每种物品可选择任意多件,故应顺序推

  20. {

  21. if(j>=w[i]) //能放第i件物品的情况

  22. dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);

  23. else //不能放第i件物品的情况

  24. dp[i][j]=dp[i-1][j];

  25. }

  26. }

  27. printf("%d\n",dp[n][W]);

  28. }

  29. return 0;

  30. }

法二:优化空间复杂度

(1)滚动使用二维数组

        结合上述递推式:dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);可知dp[i]计算时只需要dp[i-1]和dp[i],所以可以结合奇偶性,降低二维数组第一维的空间开销。

 
  1. #include <iostream>

  2. #include <cstdio>

  3. #include <cstring>

  4. using namespace std;

  5. int n,W;

  6. int w[105],v[105];

  7. int dp[2][10005]; //dp[i][j]记录从前i种物品选一些,放入容量为j的背包中可获得的最大总价值

  8. int main()

  9. {

  10. int i,j;

  11. while(scanf("%d",&n)!=EOF)

  12. {

  13. for(i=1;i<=n;i++)

  14. scanf("%d %d",&w[i],&v[i]);

  15. scanf("%d",&W);

  16. memset(dp,0,sizeof(dp));

  17. for(i=1;i<=n;i++)

  18. {

  19. for(j=1;j<=W;j++) //注意每种物品可选择任意多件,故应顺序推

  20. {

  21. if(j>=w[i]) //能放第i件物品的情况

  22. dp[i&1][j]=max(dp[(i-1)&1][j],dp[i&1][j-w[i]]+v[i]);

  23. else //不能放第i件物品的情况

  24. dp[i&1][j]=dp[(i-1)&1][j];

  25. }

  26. }

  27. printf("%d\n",dp[n&1][W]);

  28. }

  29. return 0;

  30. }

(2)降低数组维数,使用一位数组

 
  1. #include <iostream>

  2. #include <cstdio>

  3. #include <cstring>

  4. using namespace std;

  5. int n,W;

  6. int w[105],v[105];

  7. int dp[10005];

  8. int main()

  9. {

  10. int i,j;

  11. while(scanf("%d",&n)!=EOF)

  12. {

  13. for(i=1;i<=n;i++)

  14. scanf("%d %d",&w[i],&v[i]);

  15. scanf("%d",&W);

  16. memset(dp,0,sizeof(dp));

  17. for(i=1;i<=n;i++)

  18. for(j=w[i];j<=W;j++) //顺序推

  19. dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

  20. printf("%d\n",dp[W]);

  21. }

  22. return 0;

  23. }

三、多重背包

例:庆功会

为了庆贺班级在校运动会上取得全校第一名成绩,班主任决定开一场庆功会,为此拨款购买奖品犒劳运动员。期望拨款金额能购买最大价值的奖品,可以补充他们的精力和体力。

输入格式:

第一行两个数n(n<=500),m(m<=6000),其中n代表希望购买的奖品的种数,m表示拨款金额。

接下来n行,每行3个数,v、w、s,分别表示第I种奖品的价格、价值(价格与价值是不同的概念)和购买的数量(买0件到s件均可),其中v<=100,w<=1000,s<=10。

输出格式:

一个数,表示此次购买能获得的最大的价值(注意!不是价格)。

样例输入:

5 1000

80 20 4

40 50 9

30 50 7

40 30 6

20 20 1

样例输出:

1040

 
  1. #include <iostream>

  2. #include <cstdio>

  3. #include <cstring>

  4. using namespace std;

  5. const int maxn=510;

  6. const int maxm=6010;

  7. int n,m;

  8. int v[maxn],w[maxn],s[maxn];

  9. int dp[maxm]; //dp[i]:用i元钱购买奖品能获得的最大价值

  10. int main()

  11. {

  12. int i,j,k;

  13. while(scanf("%d %d",&n,&m)!=EOF)

  14. {

  15. for(i=1;i<=n;i++)

  16. scanf("%d %d %d",&v[i],&w[i],&s[i]);

  17. memset(dp,0,sizeof(dp));

  18. for(i=1;i<=n;i++) //第一重循环枚举奖品种数

  19. {

  20. for(j=m;j>=0;j--) //第二重循环枚举钱数

  21. {

  22. for(k=0;k<=s[i];k++) //第三重循环枚举购买某种物品的件数

  23. {

  24. if(j-k*v[i]>=0)

  25. dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);

  26. }

  27. }

  28. }

  29. printf("%d\n",dp[m]);

  30. }

  31. return 0;

  32. }

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/164408.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • Java多线程系列—线程通信机制wait notify notifyAll(03)

    Java多线程系列—线程通信机制wait notify notifyAll(03)线程通信机制waitnotifynotifyAll本课时我们主要学习wait/notify/notifyAll方法的使用注意事项。我们主要从三个问题入手:为什么wait方法必须在synchronized保护的同步代码中使用?为什么wait/notify/notifyAll被定义在Object类中,而sleep定义在Thread类中?wait/notify和sleep方法的异同?wait必须在synchronized保护的同步代码中使用为什么wai

  • c2框架编写

    0x00前言由于昨天520,今天又是521,我被朋友圈和qq空间给刷屏了,都在秀对象。一气之下决定把我上次写的nc拿出来使用类进行重构,多实例化几个对象,这下子我也有对象了。0x01一些小插曲

    2021年12月11日
  • 1.1音响系统放大器设计

    1.1音响系统放大器设计​⑴了解集成功率放大器内部电路工作原理;​​⑵掌握其外围电路的设计与主要性能参数的测试方法;​​⑶掌握用运放与功率管设计音频功率放大电路的方法;​​(4)掌握运用电路仿真软件进行模拟电路辅助设计的方法;

  • Java8替代传统反射动态获取成员变量值的一个示例[通俗易懂]

    Java8替代传统反射动态获取成员变量值的一个示例

  • Weka算法Clusterers-DBSCAN源代码分析

    Weka算法Clusterers-DBSCAN源代码分析

  • stm32蓝牙模块控制小车_51单片机蓝牙控制小车

    stm32蓝牙模块控制小车_51单片机蓝牙控制小车STM32库函数开发系列文章目录第一篇:STM32F103ZET6单片机双串口互发程序设计与实现第二篇:最简单DIY基于STM32单片机的蓝牙智能小车设计方案文章目录STM32库函数开发系列文章目录前言一、最简单DIY基于STM32单片机的蓝牙智能小车设计方案是什么?二、使用步骤1.准备硬件2.准备一个串口通信的代码3.修改源码三、运行与调试总结前言    daodanjishui物联网核心原创技术之最简单DIY基于STM32单片机的蓝牙智能小车设计方案。    市面上有各种开源STM3

    2022年10月10日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号