问题描述
有n个物品,第i种物品的价值为(p_i)重量为(W_i),选一些物品到一个容量为C的背包里,使得背包内物品在总重量不超过C的前提下,价值尽量大。
问题分析
在之前我们了解贪心思想的时候曾经有过类似的题目那时候物品是可拆分的我们只需要选择单位重量最大的物品即可。但是在这里,每一个物品都是完整的,只能是装或者不装。我们分析,有n个物品,为(a_1)...(a_n)当对(a_1)做决策之后,(a_2)...(a_n)是类似的问题。发现它可以用递归去解,那样的话,可以用DP去求解吗。如果可以的话我们就需要找到他的决策或者它的子问题。
针对(a_1),我们发现当(W_1)>C时,说明(a_1)无法装进背包,问题变为求(a_2)...(a_n)装进容量为C的背包求最大价值。当(W_1)<C,可以选择装入背包,则问题变为求(a_2)...(a_n)装进容量为C-(W_1)的背包,求最大价值这时候价值变为(P_1)+所求。另一种选择是不装入背包这时候问题变为求(a_2)...(a_n)装进容量为C的背包求最大价值。对于父问题(也就是这个问题)来说,最优值在这两者之间取较大值。然后把这个最优值记录下来。接下来的每一个都是这样的操作(子问题)。最终递归出口就是只剩最后一个,an...an,当(W_n)>C时不能装最大的价值就是0,(W_n)<=C时,最大价值就是(P_n)
问题求解
设dp[i][j]表示从(a_i)到(a_n)的最大价值。j为背包的剩余容量。所以j这里我们默认为了整数,潜在的,(W_i)应该也是整数。(思考:当这里不是整数的话应该怎么办?)
(dp[i][j]=left{ egin{aligned} dp[i+1][j],{W_i>C}\ max{left{ egin{aligned}dp[i+1][j],不装,{W_i<=j}\ dp[i+1][j-{W_i}]+{P_i},装,{W_i<=j}end{aligned} ight.} end{aligned} ight. )
递归出口(函数初值):
(dp[n][j]=left{ egin{aligned} 0,{W_n>j}\ {P_n},{W_n<j} end{aligned} ight. )
Java代码实现
/**
* 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数据结构,但是不能用其中的迭代器,因为迭代器不能改变在数组变化的过程中。话不多说,代码如下。
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数组,结果对比如下:
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。因为(i_1*max+j_1=i_2*max+j_2),(((i_1-i_2))=(frac{(j_2-j_1)}{max})中右侧<1,左侧在[0,n-1]中取整数,只有(i,j)完全相同时取0)。这个相当于hashmap中的hash函数来映射一样。然后其他的步骤与选择性填表的填表过程类似。代码如下:
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问题的下手点可以先分析出它的子问题,但是这个子问题是来源与决策的。所以,决策也很重要。