算法笔记:动态规划背包问题(未完待续)
来源:互联网 发布:linq 不重复数据 编辑:程序博客网 时间:2024/06/05 14:25
专题:DP背包问题
内容来源:《挑战程序设计竞赛》(第2版)+《算法竞赛入门经典》(第2版)+网上资料整理汇总
一、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。
法一:二维数组解法
#include <iostream>#include <cstdio>#include <cstring>using namespace std;const int maxn=105;const int maxv=10005;int N,W;int w[maxn],v[maxn];int dp[maxn][maxv]; //dp[i][j]记录前i件物品(部分或全部)放入容量为j的背包中可获得的最大总价值int main(){ int i,j; while(scanf("%d",&N)!=EOF) { memset(dp,0,sizeof(dp)); for(i=1;i<=N;i++) scanf("%d %d",&w[i],&v[i]); scanf("%d",&W); for(i=1;i<=N;i++) { for(j=W;j>=1;j--) { if(j>=w[i])//放第i件物品,有两种情况 dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]); else //不放第i件物品 dp[i][j]=dp[i-1][j]; } } printf("%d\n",dp[N][W]); } return 0;}
法二:优化空间复杂度
#include <iostream>#include <cstdio>#include <cstring>using namespace std;const int maxn=105;const int maxv=10005;int N,W;int w[maxn],v[maxn];int dp[maxv]; //dp[i]记录所选物品总重量不超过i时可获得的最大总价值int main(){ int i,j; while(scanf("%d",&N)!=EOF) { memset(dp,0,sizeof(dp)); for(i=1;i<=N;i++) scanf("%d %d",&w[i],&v[i]); scanf("%d",&W); for(i=1;i<=N;i++) for(j=W;j>=w[i];j--) dp[j]=max(dp[j],dp[j-w[i]]+v[i]); printf("%d\n",dp[W]); } return 0;}
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)。在此题的限制条件下便能完成。
#include <iostream>#include <cstdio>#include <cstring>using namespace std;const int maxn=105;const int maxv=105;const int INF=1000000005;int n,W;int w[maxn],v[maxn];int dp[maxn][maxn*maxv+1]; //dp[i][j]记录前i个物品挑选出价值总和为j时总重量的最小值(不存在时值为无穷大INF)int main(){ int i,j; int ret; //ret记录价值总和的最大值 while(scanf("%d",&n)!=EOF) { for(i=0;i<n;i++) scanf("%d %d",&w[i],&v[i]); scanf("%d",&W); for(i=0;i<=maxn*maxv;i++) dp[0][i]=INF; dp[0][0]=0; //表示前0个物品中什么都挑选不了,故总重量为0(自然最小值也为0) for(i=0;i<n;i++) { for(j=0;j<=maxn*maxv;j++) { if(j<v[i]) //第i件物品的价值比价值总和j还大,则不能选第i件物品 dp[i+1][j]=dp[i][j]; else //否则可以选也可以不选第i件物品 dp[i+1][j]=min(dp[i][j],dp[i][j-v[i]]+w[i]); } } ret=0; for(i=0;i<=maxn*maxv;i++) //扫描价值总和,找解 { if(dp[n][i]<=W) //n件物品中挑选出的物品价值总和为i时,总重量<=W,则更新ret ret=i; } printf("%d\n",ret); } return 0;}
二、完全背包
有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]},将这个方程用一维数组实现,便得到了上面的伪代码。
法一:
#include <iostream>#include <cstdio>#include <cstring>using namespace std;int n,W;int w[105],v[105];int dp[105][10005]; //dp[i][j]记录从前i种物品选一些,放入容量为j的背包中可获得的最大总价值int main(){ int i,j; while(scanf("%d",&n)!=EOF) { for(i=1;i<=n;i++) scanf("%d %d",&w[i],&v[i]); scanf("%d",&W); memset(dp,0,sizeof(dp)); for(i=1;i<=n;i++) { for(j=1;j<=W;j++) //注意每种物品可选择任意多件,故应顺序推 { if(j>=w[i]) //能放第i件物品的情况 dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]); else //不能放第i件物品的情况 dp[i][j]=dp[i-1][j]; } } printf("%d\n",dp[n][W]); } return 0;}
法二:优化空间复杂度
(1)滚动使用二维数组
结合上述递推式:dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);可知dp[i]计算时只需要dp[i-1]和dp[i],所以可以结合奇偶性,降低二维数组第一维的空间开销。
#include <iostream>#include <cstdio>#include <cstring>using namespace std;int n,W;int w[105],v[105];int dp[2][10005]; //dp[i][j]记录从前i种物品选一些,放入容量为j的背包中可获得的最大总价值int main(){ int i,j; while(scanf("%d",&n)!=EOF) { for(i=1;i<=n;i++) scanf("%d %d",&w[i],&v[i]); scanf("%d",&W); memset(dp,0,sizeof(dp)); for(i=1;i<=n;i++) { for(j=1;j<=W;j++) //注意每种物品可选择任意多件,故应顺序推 { if(j>=w[i]) //能放第i件物品的情况 dp[i&1][j]=max(dp[(i-1)&1][j],dp[i&1][j-w[i]]+v[i]); else //不能放第i件物品的情况 dp[i&1][j]=dp[(i-1)&1][j]; } } printf("%d\n",dp[n&1][W]); } return 0;}
(2)降低数组维数,使用一位数组
#include <iostream>#include <cstdio>#include <cstring>using namespace std;int n,W;int w[105],v[105];int dp[10005];int main(){ int i,j; while(scanf("%d",&n)!=EOF) { for(i=1;i<=n;i++) scanf("%d %d",&w[i],&v[i]); scanf("%d",&W); memset(dp,0,sizeof(dp)); for(i=1;i<=n;i++) for(j=w[i];j<=W;j++) //顺序推 dp[j]=max(dp[j],dp[j-w[i]]+v[i]); printf("%d\n",dp[W]); } return 0;}
三、多重背包
例:庆功会
为了庆贺班级在校运动会上取得全校第一名成绩,班主任决定开一场庆功会,为此拨款购买奖品犒劳运动员。期望拨款金额能购买最大价值的奖品,可以补充他们的精力和体力。
输入格式:
第一行两个数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
#include <iostream>#include <cstdio>#include <cstring>using namespace std;const int maxn=510;const int maxm=6010;int n,m;int v[maxn],w[maxn],s[maxn];int dp[maxm]; //dp[i]:用i元钱购买奖品能获得的最大价值int main(){ int i,j,k; while(scanf("%d %d",&n,&m)!=EOF) { for(i=1;i<=n;i++) scanf("%d %d %d",&v[i],&w[i],&s[i]); memset(dp,0,sizeof(dp)); for(i=1;i<=n;i++) //第一重循环枚举奖品种数 { for(j=m;j>=0;j--) //第二重循环枚举钱数 { for(k=0;k<=s[i];k++) //第三重循环枚举购买某种物品的件数 { if(j-k*v[i]>=0) dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]); } } } printf("%d\n",dp[m]); } return 0;}
- 算法笔记:动态规划背包问题(未完待续)
- 背包问题总结(未完待续)
- 算法笔记:数学问题+数论初步一(未完待续)
- 基础算法2——动态规划(dynamic programming)【未完待续】
- 算法笔记:贪心法(未完待续)
- 动态规划算法,背包问题
- 算法-动态规划-背包问题
- 《背包九讲》学习笔记(未完待续)
- 动态规划整理总结(未完待续)
- 算法学习笔记4-动态规划-背包问题
- 算法学习笔记4-动态规划-背包问题
- 经典算法题04-动态规划算法(背包问题)
- 算法导论学习笔记(十三):动态规划(三):01背包问题
- 一些算法(未完待续)
- 背包问题(动态规划)
- 背包问题(动态规划)
- 背包问题(动态规划)
- 背包问题(动态规划)
- 加密解密算法java实现(5)—RSA 加解密的例子
- c++面向对象编程
- SetBkMode函数理解
- .List,Set,Map是否继承自Collection接口?
- 高仿微信主界面
- 算法笔记:动态规划背包问题(未完待续)
- 移动平台 Unity3D 应用性能优化
- oracle11g数据库中的json工具
- 测试信息
- Python 错误和异常
- string类的引用计数的写时拷贝分析
- BZOJ2982: combination(Lucas定理)
- [Suzuki85]轮廓跟踪算法论文翻译
- 老蜗牛写采集:获取数据(正则篇)