Loading [MathJax]/jax/output/CommonHTML/autoload/mtable.js

DP动态规划之01背包问题

问题描述

有n个物品,第i种物品的价值为pipi重量为WiWi,选一些物品到一个容量为C的背包里,使得背包内物品在总重量不超过C的前提下,价值尽量大。

问题分析

 在之前我们了解贪心思想的时候曾经有过类似的题目那时候物品是可拆分的我们只需要选择单位重量最大的物品即可。但是在这里,每一个物品都是完整的,只能是装或者不装。我们分析,有n个物品,为a1a1...anan当对a1a1做决策之后,a2a2...anan是类似的问题。发现它可以用递归去解,那样的话,可以用DP去求解吗。如果可以的话我们就需要找到他的决策或者它的子问题。
 针对a1a1,我们发现当W1W1>C时,说明a1a1无法装进背包,问题变为求a2a2...anan装进容量为C的背包求最大价值。当W1W1<C,可以选择装入背包,则问题变为求a2a2...anan装进容量为C-W1W1的背包,求最大价值这时候价值变为P1P1+所求。另一种选择是不装入背包这时候问题变为求a2a2...anan装进容量为C的背包求最大价值。对于父问题(也就是这个问题)来说,最优值在这两者之间取较大值。然后把这个最优值记录下来。接下来的每一个都是这样的操作(子问题)。最终递归出口就是只剩最后一个,an...an,当WnWn>C时不能装最大的价值就是0,WnWn<=C时,最大价值就是PnPn

问题求解

设dp[i][j]表示从aiaianan的最大价值。j为背包的剩余容量。所以j这里我们默认为了整数,潜在的,WiWi应该也是整数。思考:当这里不是整数的话应该怎么办?)
 
dp[i][j]={dp[i+1][j],Wi>Cmax{dp[i+1][j],,Wi<=jdp[i+1][jWi]+Pi,,Wi<=j
 
递归出口(函数初值):
dp[n][j]={0,Wn>jPn,Wn<j

Java代码实现

Copy
/** * DP问题处理01背包问题 * @param w 物品重量 * @param p 物品价值 * @param C 背包容量 * @param n 物品个数 */ public static void test(int[] w,int[] p,int C,int n) { int[][]dp=new int[n+1][C+1]; //初值 for(int i=0;i<=C;i++) if(i>=w[n]) dp[n][i]=p[n]; //2~n-1 for(int i=n-1;i>=2;i--) { for(int j=0;j<=C;j++) { if(w[i]>j) dp[i][j]=dp[i+1][j]; else { int temp=dp[i+1][j-w[i]]+p[i]; if(temp>dp[i+1][j]) dp[i][j]=temp; else dp[i][j]=dp[i+1][j]; } } } //求1,C if(w[1]>C) dp[1][C]=dp[2][C]; else { int temp=dp[2][C-w[1]]+p[1]; if(temp>dp[2][C]) dp[1][C]=temp; else dp[1][C]=dp[2][C]; } System.out.println("最优值为"+dp[1][C]); //最优解 int j=C; for(int i=1;i<n;i++) { if(dp[i][j]==dp[i+1][j]) { System.out.println(i+"不放"); } else { System.out.println(i+"放"); j=j-w[i]; } } if(dp[n][j]==0) System.out.println(n+"不放"); else System.out.println(n+"放"); }

优化方向一:时间方面:因为是j是整数是跳跃式的,可以选择性的填表。

(DP它避免了很多重复计算,但有时候会计算无用的子问题就是做了许多无用计算。可以以这种思想进行优化。)

 主要想法就是记录计算的路径。因为对于需要计算的每一个(i,j)如果&W_i&<j那么就需要计算(i+1,j)和(i+1,j-&W_i&)。从(1,n)开始推,因为不知道数组的长度选取了ArrayList数据结构,但是不能用其中的迭代器,因为迭代器不能改变在数组变化的过程中。话不多说,代码如下。

Copy
public static void test(int[] w,int[] p,int C,int n) { class Point{ int x; int y; public Point(int x, int y) { this.x = x; this.y = y; } } //计算所用路径 int[][]flag=new int[n+1][C+1]; ArrayList<Point> list=new ArrayList<Point>(); list.add(new Point(1,C)); int iter=0; while(iter!=list.size()) { Point temp = list.get(iter++); if(temp.x>=n) break; if(flag[temp.x+1][temp.y]==0)//没有添加过 { list.add(new Point(temp.x+1,temp.y)); flag[temp.x+1][temp.y]=1; } if(temp.y-w[temp.x]>0&&flag[temp.x+1][temp.y-w[temp.x]]==0) { list.add(new Point(temp.x+1,temp.y-w[temp.x])); flag[temp.x+1][temp.y-w[temp.x]]=1; } } int[][]dp=new int[n+1][C+1]; for(int i=list.size()-1;i>=0;i--) { Point temp=list.get(i); if(temp.x==n) dp[temp.x][temp.y]=w[temp.x]>temp.y?0:p[temp.x]; else { if(w[temp.x]>temp.y)//装不下 dp[temp.x][temp.y]=dp[temp.x+1][temp.y]; else { int t=dp[temp.x+1][temp.y-w[temp.x]]+p[temp.x]; dp[temp.x][temp.y]=t>dp[temp.x+1][temp.y]?t:dp[temp.x+1][temp.y]; } } } for(int i=1;i<dp.length;i++) { for(int j=1;j<dp[0].length;j++) { System.out.print(dp[i][j]+" "); } System.out.println(); } System.out.println("最优值为"+dp[1][C]); //最优解 int j=C; for(int i=1;i<n;i++) { if(dp[i][j]==dp[i+1][j]) { System.out.println(i+"不放"); } else { System.out.println(i+"放"); j=j-w[i]; } } if(dp[n][j]==0) System.out.println(n+"不放"); else System.out.println(n+"放"); }

输出了不同的dp数组,结果对比如下:

Copy
public static void main(String[] args) { int[] w= {0,2,1,2,3,3}; int[] p= {0,10,8,9,12,4}; test(w,p,7,5); }

优化前:

优化后:

思考二:处理j(背包容量),w(重量)不为整数的时候,因为j不为整数了,它就没办法作为数组下标使用。

 主要思想:这个想法建立在选择性填表的优化之上,在做选择性填表这个优化的时候,我们将计算路径记录了下来,现在我们也一样先记录计算路径,只不过Point(内部类)中的y值用float表示。然后将dp这个表变为map的数据结构。使用map保存是最重要的就是键的确定,这个键需要保存对应到dp中(i,j)这两个信息。key=i*max+j,当max=max(max(w[i]),C)+1时,针对每一个(i,j)都有确定的唯一key。因为i1max+j1=i2max+j2,((i1i2)=(j2j1)max中右侧<1,左侧在[0,n-1]中取整数,只有(i,j)完全相同时取0)。这个相当于hashmap中的hash函数来映射一样。然后其他的步骤与选择性填表的填表过程类似。代码如下:

Copy
public static void test(float[] w,float[] p,float C,int n) { class Point{ int x; float y; public Point(int x, float y) { this.x = x; this.y = y; } } //计算函数表达式系数 float max=w[0]; for(int i=1;i<w.length;i++) { if(w[i]>max) max=w[i]; } max=Math.max(C, max)+1; //计算所用路径 ArrayList<Point> list=new ArrayList<Point>(); Map<Float,Float> map=new HashMap<Float,Float>(); list.add(new Point(1,C)); int iter=0; while(iter!=list.size()) { Point temp = list.get(iter++); if(temp.x>=n) break; if(!map.containsKey((temp.x+1)*max+temp.y)) { list.add(new Point(temp.x+1,temp.y)); map.put((temp.x+1)*max+temp.y, (float) -1.0); } if(temp.y-w[temp.x]>=0&&!map.containsKey((temp.x+1)*max+(temp.y-w[temp.x]))) { list.add(new Point(temp.x+1,temp.y-w[temp.x])); map.put((temp.x+1)*max+(temp.y-w[temp.x]), (float) -1.0); } } //填表 for(int i=list.size()-1;i>=0;i--) { Point t=list.get(i); if(t.x==n) { if(w[t.x]>t.y)map.put(t.x*max+t.y, (float) 0); else map.put(t.x*max+t.y, p[t.x]); } else { if(w[t.x]<=t.y) { float ft=p[t.x]+map.get((t.x+1)*max+(t.y-w[t.x])); if(ft>map.get((t.x+1)*max+t.y)) map.put(t.x*max+t.y, ft); else map.put(t.x*max+t.y, map.get((t.x+1)*max+t.y)); } else map.put(t.x*max+t.y, map.get((t.x+1)*max+t.y)); } } System.out.println("最优值:"+map.get(max+C)); //最优解 float j=C; for(int i=1;i<n;i++) { if(map.get(i*max+j)==map.get((i+1)*max+j))//说明没装 System.out.println(i+"不放"); else { System.out.println(i+"放"); j=j-w[i]; } } if(map.get(n*max+j)==0) System.out.println(n+"不放"); else System.out.println(n+"放"); }

总结

首先,这个问题的解决从递归到递推,因为i(开始的物品)和j(背包)的存在选用二维数组(i..n,n是一定的,所以只有两个参数)。
其次,我一开始想的是倒着推,从只有一个物品到n个这也是DP问题常用的想法,就是从小问题到大问题。
最后,DP问题的下手点可以先分析出它的子问题,但是这个子问题是来源与决策的。所以,决策也很重要。

posted @   小帆敲代码  阅读(586)  评论(0)    收藏  举报
编辑推荐:
· 记一次 .NET某旅行社酒店管理系统 卡死分析
· 长文讲解 MCP 和案例实战
· Hangfire Redis 实现秒级定时任务,使用 CQRS 实现动态执行代码
· Android编译时动态插入代码原理与实践
· 解锁.NET 9性能优化黑科技:从内存管理到Web性能的最全指南
阅读排行:
· 工良出品 | 长文讲解 MCP 和案例实战
· 一天 Star 破万的开源项目「GitHub 热点速览」
· 多年后再做Web开发,AI帮大忙
· 记一次 .NET某旅行社酒店管理系统 卡死分析
· 别再堆文档了,大模型时代知识库应该这样建
点击右上角即可分享
微信分享提示