大家好,欢迎来到IT知识分享网。
一、概述
动态规划(DP)算法是初学者的一个难点。思考DP问题时,核心思路仍和其它算法类似,将复杂问题分解为相对更简单的问题。简单说,一个问题规模N的问题是否能分解成N-1的问题(递归)?或者能否从规模1开始推导得到规模N(递推和DP)。
背包算法就是一种典型的从规模1推导到规模N的算法,是最常见的一种DP算法。它的核心要素有三个:背包容量,物品重量(或体积),物品价值,题目一般会要求在背包容量限制下获取最大价值。最简单的背包问题,有N件物品,每件物品都有一个重量和价值,在背包空间有限情况下如何选择物品,使得价值最大。
这类问题显然不能用贪心思想解决。可以把物品编号从1……N,通过从物品1开始推导的方式解决。(1)只有物品1,背包够大时最大价值就是物品1的价值。(2)物品2拿过来,此时背包空间够大显然应该拿取物品1和2,如果不够大就要做一个取舍。(3)当物品3拿过来时,此时就有4种前置状态:空包、只有物品1,只有物品2,物品1和物品2。但如果这样处理,就演变成一个搜索问题,复杂度为2^N。搜索算法会计算大量无效状态,效率极低。背包算法的巧妙之处在于:通过计算“不同的背包空间”下拿取前i个物品的最大价值,避免了大量无效的搜索,从而把复杂度降为N^2。
背包三要素可能表现为多种形式,比如背包容量是时间(以下题目均在洛谷)(P1048 采药),体力(P1510 精卫填海),数值(P1734 最大约数和)。物品重量和背包容量在同一题目中的概念总是一致的。
背包问题主要有四种基础类型:01背包、完全背包、分组背包、多重背包。在此之上有针对背包容量进行升级的二维背包(有两个不同的容量限制)、有针对物品种类进行升级的混合背包(比如有些物品01,有些物品多重),有存在依赖关系的背包,比如拿B物品必须拿A物品。
从本文作者的经验看,只要掌握了01背包的二维写法,其他所有背包问题都可以通过此算法扩展出来。换句话说,所有的背包问题都是01背包的拓展。比如说有依赖关系的物品A和B(拿B必须拿A),可以想象成是两件物品做01背包,一件是A,一件是(A+B)。
二、01背包
01背包指的是n个物品都是唯一的,我们对每一件物品只有选择或者不选择两种操作。01背包是所有其他背包问题的基础。通过转换思维方式,可以用01背包的二维数组解法解出所有的背包问题。 01背包的基础作法与其他动规类题目类似,每一次循环完成一个物品的计算工作。用二维数组dp[i][j]表示拿完第i件物品时j背包容量的最大值或方案数。
在物品价值方面,如果题目给了价值或者价值计算方法(P1060 开心的金明),那么我们写dp方程时按题目要求添加即可。如果没有给物品价值,只是让我们求能填充的最大容量或者求方案数,我们可以将dp数组的0号单元置1,dp[0]=1,便于推导最大容量或方案数。
#include <bits/stdc++.h> using namespace std; int t,m,dp[105][1005],ti[105],val[105]; int main() { int i,j; cin>>t>>m; for(i=1; i<=m; i++) cin>>ti[i]>>val[i]; for(i=1; i<=m; i++) { for(j=0; j<=t; j++) { if(j<ti[i]) dp[i][j]=dp[i-1][j]; else /< dp[i-1][j]不选择i物品,dp[i-1][j-ti[i]]+val[i]选择 */ dp[i][j]=max(dp[i-1][j],dp[i-1][j-ti[i]]+val[i]); } } cout<<dp[m][t]; return 0; }
01背包问题完全体现了dp算法的特色,在计算第i行时只考虑第i-1行的值。01背包有一种优化技巧,即可以通过滚动数组的方法减少存储空间。此时只需使用一个一维数组即可完成背包计算。学习此类算法可以通过每一次循环打印dp数组来加深理解。
#include <bits/stdc++.h> using namespace std; int t,m,dp[1005],ti[105],val[105]; int main() { int i,j; cin>>t>>m; for(i=1; i<=m; i++) cin>>ti[i]>>val[i]; for(i=1; i<=m; i++)/< 特别注意滚动的方向必须从大到小 */ for(j=t; j>=ti[i]; j--) /< dp[j]不选择i物品,dp[j-ti[i]]+val[i]选择i */ dp[j]=max(dp[j],dp[j-ti[i]]+val[i]); cout<<dp[t]; return 0; }
三、完全背包
完全类似于01背包问题,所不同的是每种物品有无限件,如果V是背包容量,c是物品重量,那么从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……取[V/c]件等多种方案。如果仍然按照解01背包时的思路,每次解决一种物品的价值计算问题,我们仍可以采用二维数组来推导,但是在推导第i件物品的价值时,我们需要用0件、1件、2件……[V/c]件依次去更新dp[i][0]……dp[i][V],这样显然效率比较差。下面为信息学奥赛一本通(C++版)在线评测系统【例9.12】完全背包问题的基础代码。
#include <bits/stdc++.h> using namespace std; int dp[1000][1000],w[1000],v[1000],m,n,ans=0; int main() /< m是背包容量,n是物品数量,w数组是重量,v数组是价值 */ { int i,j,k; cin>>m>>n; for(i=1;i<=n;i++) cin>>w[i]>>v[i]; for(i=1;i<=n;i++) { for(j=0;j<=m;j++) {/< 对于空间大小为j,尝试拿去第i件物品,拿0件就是dp[i-1][j] ,最多拿件数j/w[i]*/ for(k=0;k<=j/w[i];k++) dp[i][j]=max(dp[i][j], dp[i-1][j-k*w[i]]+k*v[i]); } } cout<<"max="<<dp[n][m]; return 0; }
可以采用滚动计算的方式来降低复杂度为平方级。
#include <bits/stdc++.h> using namespace std; int i,j,T,n,t[101],p[101],dp[101][1001]; int main() { scanf("%d%d",&T,&n); for(i=1;i<=n;i++) scanf("%d%d",&t[i],&p[i]); for(i=1;i<=n;i++) { for(j=0;j<=T;j++) /< 先获取上一层数据,作为不选择i物品的基础数据 */ dp[i][j]=dp[i-1][j]; for(j=t[i];j<=T;j++) /< 滚动方式试探拿去i物品是否能获取更大价值 */ dp[i][j]=max(dp[i][j],dp[i][j-t[i]]+p[i]); } printf("max=%d",dp[n][T]); return 0; }
对于背包问题的代码理解,个人建议每次背包计算之后都输出dp数组,观察dp数组变化的过程。从上面代码看很明显每次dp[i][j]都先从dp[i-1][j]获取值,因此完全背包同样可以使用一维数组滚动计算,此时正好和01背包循环次序相反。
#include <bits/stdc++.h> using namespace std; int v,t,m,dp[],ti[10005],val[10005]; int main() /< P1616 疯狂的采药 */ { int i,j; cin>>t>>m; for(i=1; i<=m; i++) cin>>ti[i]>>val[i]; for(i=1; i<=m; i++) /< 为实现1件,2件....t/ti件物品,完全背包从小到大滚动 */ for(j=ti[i]; j<=t; j++) dp[j]=max(dp[j],dp[j-ti[i]]+val[i]); cout<<dp[t]; return 0; }
四、分组背包
分组背包和多重背包实际上属于同一类问题,也可以看成是有条件(同组只能选一个)的01背包。从理解问题的角度看,二维dp数组是解决背包问题的万能钥匙。和前面不同的是,同一组物品我们认为是一件物品,一样有01两种选择。dp[i][j]表示的是背包容量为j时拿完第i组物品后的最大价值,当我们计算dp[i][j]时,需要找到所有组号为i的物品分别尝试。
#include <bits/stdc++.h> using namespace std; /< 一本通1272:【例9.16】分组背包 */ int v,n,t,a[1005],b[1005],c[1005],f[15][205]; int main() { int i,j,k,minn=INT_MAX; cin>>v>>n>>t; for(i=1; i<=n; i++) cin>>a[i]>>b[i]>>c[i]; for(i=1; i<=t; i++) /< 分组的01背包,一组一组拿 */ { for(j=v; j>=0; j--) f[i][j]=f[i-1][j];/< 初始化第k组价值为前一组,有可能这一组不能拿物品(价值太低) */ for(k=1; k<=n; k++) /< 循环n件物品 */ if(c[k]==i) /< 物品属于第i组 */ for(j=v; j>=a[k]; j--) /< 用第i件物品去试探能否获得更大价值 */ f[i][j]=max(f[i][j],f[i-1][j-a[k]]+b[k]); } cout<<f[t][v]; return 0; }
同样,分组背包也可以用一维数组。
#include <bits/stdc++.h> using namespace std; int n,m,dp[],a[10005],b[10005],c[10005]; int main() /< P1757 通天之分组背包,80分代码 */ { int i,j,k; cin>>m>>n; for(i=1; i<=n; i++) cin>>a[i]>>b[i]>>c[i]; for(k=1; k<=n; k++) /< k表示组号 */ for(j=m;j>=0;j--) /< 对于每一个dp[j],我们用k组的全部物品计算 因为是01背包,切记j必须从大到小*/ for(i=1;i<=n;i++) if(c[i]==k&&a[i]<=j) dp[j]=max(dp[j],dp[j-a[i]]+b[i]); cout<<dp[m]; return 0; }
五、多重背包
多重背包,物品的数量既不是一个,也不是无限,在拿取物品的时候只能选择0到K个。可以把多重背包看成是分组背包,每一件物品都是一组,把选取1个,2个….K个看成是同一组的不同的物品,这样多重背包就和分组背包有极其相似的代码。
多重背包在暴力枚举第i件物品的每一种可能数量时复杂度较高,它还有一种极为巧妙的二进制优化方法,比如第i件物品最多可以拿20个,把这20件物品拆分成1、2 、4、 8、 5,这样我们得到5件不同的物品,对其做01背包。读者可以思考下,1、2、4、8对应二进制后4位,可以搭配出1…15间任意数量,再加上一个5,可以搭配出1……20间任意数量。用01背包的方法计算时,最终的数量选择(最优值),一定会被二进制拆分的数量组合出来。
#include <bits/stdc++.h> using namespace std; int n,m,a[1005],dp[1005]; int main() /< P1077 摆花 */ { cin>>n>>m; int i,j,k; for(i=1; i<=n; i++) cin>>a[i]; dp[0]=1; for(i=1; i<=n; i++) /< i件物品 */ { for(j=m; j>=0; j--) { /< 认为第i种物品拿1,2...ai盆属于不同的物品 */ for(k=1;k<=a[i]&&k<=j;k++) dp[j]=(dp[j]+dp[j-k])%; } } cout<<dp[m]; return 0; }
六、背包价值
前面提到过背包问题一般都是求最大价值,还有一些求能填充的最大容量或者求方案数问题。两类问题最大的区别在于求dp的代码。 求价值:dp[j]=max(dp[j],dp[j-a[i]]+b[i]);用空间换价值。 求容量一般(设置dp[0]=1),只需标注是否能到达某个容量dp[j]=dp[j]+dp[j-a[i],如果能到达dp[j]值非0。求方案数(设置dp[0]=1),当前方案累加前一种情况方案,dp[j]=(dp[j]+dp[j-a[i])%;现有的dp[j]方案数累加上由i物品新增的方案数,方案数问题很容易超范围,一般都会要求求余,为稳妥起见,建议使用longlong类型。
七、小结
背包问题的学习需要大量练习,才能掌握发现背包三要素的诀窍。 例如P2066 机器分配,我们可以把每个公司的不同使用时间看成是分组背包,因为这一组里面只能每个公司只能选一种机器使用时间,确定背包类型后,用分组的一维算法还是二维算法都可以解决这一问题,解决思路会比较清晰。
下面有一个习题读者可以看看是这是什么背包问题(答案为本文概述中彩色文字)
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/116098.html