0/1背包问题(递归解决,递推解决)

0-1背包问题: 

有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

 这个问题的特点是:每种物品只有一件,可以选择放或者不放。

输入格式:
V,N
W1,V1
W2,V2
......
输出格式: 
X

帮助理解:

比如01背包问题。

/* 一个旅行者有一个最多能用M公斤的背包,现在有N件物品,
它们的重量分别是W1,W2,...,Wn,
它们的价值分别为P1,P2,...,Pn.
若每种物品只有一件求旅行者能获得最大总价值。
输入格式:
M,N
W1,P1
W2,P2
......
输出格式: 

*/

因为背包最大容量M未知。所以,我们的程序要从1到M一个一个的试。比如,开始任选N件物品的一个。看对应M的背包,能不能放进去,如果能放进去,并且还有多的空间,则,多出来的空间里能放N-1物品中的最大价值。怎么能保证总选择是最大价值呢?看下表。

测试数据:
10,3
3,4
4,5
5,6

c[i][j]数组保存了1,2,3号物品依次选择后的最大价值.

这个最大价值是怎么得来的呢?从背包容量为0开始,1号物品先试,0,1,2,的容量都不能放.所以置0,背包容量为3则里面放4.这样,这一排背包容量为4,5,6,....10的时候,最佳方案都是放4.假如1号物品放入背包.则再看2号物品.当背包容量为3的时候,最佳方案还是上一排的最价方案c为4.而背包容量为5的时候,则最佳方案为自己的重量5.背包容量为7的时候,很显然是5加上一个值了。加谁??很显然是7-4=3的时候.上一排 c3的最佳方案是4.所以。总的最佳方案是5+4为9.这样.一排一排推下去。最右下放的数据就是最大的价值了。(注意第3排的背包容量为7的时候,最佳方案不是本身的6.而是上一排的9.说明这时候3号物品没有被选.选的是1,2号物品.所以得9.)

从以上最大价值的构造过程中可以看出。

f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}


递归解决:(主要思考下面的递归公式是否能包含所有情况)

背包问题具有最优子结构,令f(n,C)代表,有n个待选物品,背包容量为C时的最优解,此时物品选择向量为y=[y1,y2,…yn], 那么当yn=1时,y’=[y1, y2, …yn-1],必然为f(n-1, C-wn)的物品选择向量,当yn=0时,必然为f(n-1,C)的最优物品选择向量。所以背包问题可以由动态规划来求解。

    根据上面的分析,我们可以得到如下的递归式:

    当wn>C时,  f(n,C)=f(n+1,C);

    当wn<=C时,f(n,C) = max(f(n+1,C), vn+f(n+1, C-wn) );//f(n+1,C)当前物品没有选择,vn+f(n+1, C-wn)选择了当前物品

    初始条件为:f(i, 0) = 0; f(0,i) = 0; f(0,0) = 0;

说明:如果理解不动,可以参考下面动态规划实现中的那个图仔细想想

程序实现:

import java.util.Scanner;

//递归解决的背包问题
public class BeiBao01DG {

    private static int rl;//背包容量
    private static int num;//物品个数
    private static int[] w;//物品重量(重量和价值一下标相关联)
    private static int[] v;//物品价值
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Scanner sc=new Scanner(System.in);
        rl=sc.nextInt();
        num=sc.nextInt();
        w=new int[num];
        v=new int[num];
        for(int i=0;i         {
            w[i]=sc.nextInt();
            v[i]=sc.nextInt();
        }
        System.out.println(f(0,rl));
        
    }
    //cur(0---num-1)代表物品编号, max代表从第一个物品到现在的物品背包还剩下的空间
    public static int f(int cur,int max)
    {
        if(cur==num)//物品已经尝试完
         return 0;
        if(max<=0)//背包已经装满,没有容量了
            return 0;
        if(max             return f(cur+1,max);
        else
        {
            return Math.max(f(cur+1,max), f(cur+1,max-w[cur])+v[cur]);
        }
    }

}

注意:此处物品可以随便输入,不用排序,结果没有影响,递归是拿着背包容量向下推的,动态规划已知前面的推出后面的,需要找状态转换公式,也就是根据递归公式来得到状态转换公式


扩展问题:

输出哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大???


动态规划解决:

算法基本思想:

利用动态规划思想 ,子问题为:f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。

其状态转移方程是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}    //这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。

解释一下上面的方程:“将前i件物品放入容量为v的背包中”这个子问题,如果只考虑第i件物品放或者不放,那么就可以转化为只涉及前i-1件物品的问题,即1、如果不放第i件物品,则问题转化为“前i-1件物品放入容量为v的背包中”;2、如果放第i件物品,则问题转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”(此时能获得的最大价值就是f [i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i])。则f[i][v]的值就是1、2中最大的那个值。

(注意:f[i][v]有意义当且仅当存在一个前i件物品的子集,其费用总和为v。所以按照这个方程递推完毕后,最终的答案并不一定是f[N] [V],而是f[N][0..V]的最大值。)

优化空间复杂度:

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

上面f[i][v]使用二维数组存储的,可以优化为一维数组f[v],将主循环改为:

for i=1..N

for v=V..0

f[v]=max{f[v],f[v-c[i]]+w[i]};

即将第二层循环改为从V..0,逆序。

解释一下:

假设最大容量M=10,物品个数N=3,物品大小w{3,4,5},物品价值p{4,5,6}。

当进行第i次循环时,f[v]中保存的是上次循环产生的结果,即第i-1次循环的结果(i>=1)。所以f[v]=max{f[v],f[v-c[i]]+w[i]}这个式子中,等号右边的f[v]和f[v-c[i]]+w[i]都是前一次循环产生的值。

当i=1时,f[0..10]初始值都为0。所以

f[10]=max{f[10],f[10-c[1]]+w[1]}=max{0,f[7]+4}=max{0,0+4}=4;

f[9]=max{f[9],f[9-c[1]]+w[1]}=max{0,f[6]+4}=max{0,0+4}=4;

......

f[3]=max{f[3],f[3-c[1]]+w[1]}=max{0,f[3]+4}=max{0,0+4}=4;

f[2]=max{f[2],f[2-c[1]]+w[1]}=max{0,f[2-3]+4}=0;//数组越界?

f[1]=0;

f[0]=0;

当i=2时,此时f[0..10]经过上次循环后,都已经被重新赋值,即f[0..2]=0,f[3..10]=4。利用f[v]=max{f[v],f[v-c[i]]+w[i]}这个公式计算i=2时的f[0..10]的值。

当i=3时同理。

具体的值如下表所示:

因此,利用逆序循环就可以保证在计算f[v]时,公式f[v]=max{f[v],f[v-c[i]]+w[i]}中等号右边的f[v]和f[v-c[i]]+w[i]保存的是f[i-1][v]和f[i -1][v-c[i]]的值

当i=N时,得到的f[V]即为要求的最优值。

初始化的细节问题:

 在求最优解的背包问题中,一般有两种不同的问法:1、要求恰好装满背包时的最优解;2、求小于等于背包容量的最优解,即不一定恰好装满背包。

这两种问法,在初始化的时候是不同的。

1、要求恰好装满背包时的最优解:

在初始化时除了f[0]0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。如果不能恰好满足背包容量,即不能得到f[V]的最优值,则此时f[V]=-∞,这样就能表示没有找到恰好满足背包容量的最优值。

2、求小于等于背包容量的最优解,即不一定恰好装满背包:

如果并没有要求必须把背包装满,而是只希望价值尽量大,初始化时应该将f[0..V]全部设为0。

总结

01背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转换成01背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。

 

0-1背包问题代码:

代码1
复制代码
#include 
#include 
using namespace std;
const int MIN=0x80000000;
const int N=3;   //物品数量
const int V=5;  //背包容量
int f[N+1][V+1];

int Package(int *W,int *C,int N,int V);
void main(int argc,char *argv[])
{
 int W[4]={0,7,5,8};      //物品权重
 int C[4]={0,2,3,4};      //物品大小
 int result=Package(W,C,N,V);
 if(result>0)
 {
  cout<"the opt value:"<int i=N,j=V;
  while(i)
  {
   if(f[i][j]==(f[i-1][j-C[i]]+W[i]))
   {
    cout<":"<<"w="<",c="<else
  cout<<"can not find the opt value"<return;
}

int Package(int *W,int *C,int N,int V)
{
 int i,j;
 memset(f,0,sizeof(f));  //初始化为0

 for(i=0;i<=N;i++)
 for(j=1;j<=V;j++)               //此步骤是解决是否恰好满足背包容量,
  f[i][j]=MIN;                //若“恰好”满足背包容量,即正好装满背包,则加上此步骤,若不需要“恰好”,则初始化为0
    
 for(i=1;i<=N;i++)
  for(j=C[i];j<=V;j++)
  {
   f[i][j]=(f[i-1][j]>f[i-1][j-C[i]]+W[i])?f[i-1][j]:(f[i-1][j-C[i]]+W[i]);
   cout<<"f["<"]["<"]="<return f[N][V];
}
复制代码
代码2
复制代码
#include 
#include 
using namespace std;
const int MIN=0x80000000;
const int N=3;   //物品数量
const int V=5;  //背包容量
int f[V+1];

int Package(int *W,int *C,int N,int V);
void main(int argc,char *argv[])
{
 int W[4]={0,7,5,8};      //物品权重
 int C[4]={0,2,3,4};      //物品大小
 int result=Package(W,C,N,V);
 if(result>0)
 {
  cout<"the opt value:"<else
  cout<<"can not find the opt value"<return;
}

int Package(int *W,int *C,int N,int V)
{
 int i,j;
 memset(f,0,sizeof(f));  //初始化为0

 for(i=1;i<=V;i++)               //此步骤是解决是否恰好满足背包容量,
  f[i]=MIN;                //若“恰好”满足背包容量,即正好装满背包,则加上此步骤,若不需要“恰好”,则初始化为0
    
 for(i=1;i<=N;i++)
  for(j=V;j>=C[i];j--)    //注意此处与解法一是顺序不同的,弄清原因
  {
   f[j]=(f[j]>f[j-C[i]]+W[i])?f[j]:(f[j-C[i]]+W[i]);
   cout<<"f["<"]["<"]="<return f[V];
}

参考网站:http://www.cnblogs.com/justinzhang/archive/2012/04/10/2441199.html


你可能感兴趣的:(递归,Java基础,动态规划)