I am a teacher!

导航

背包问题(1):基本模型和解法

        背包问题的基本模型是:

        有一个容量为C的背包,现在要从N件物品中选取若干件装入背包中,每件物品i的重量为W[i]、价值为P[i]。定义一种可行的背包装载为:背包中物品的总重不能超过背包的容量,并且一件物品要么全部选取、要么不选取。定义最佳装载是指所装入的物品价值最高,并且是可行的背包装载。

        例如,设C= 12,N=4,W[4]={2,4,6,7},P[4]={ 6,10,12,13},则装入W[1]和W[3],最大价值为23。

        若采用贪心法来解决0/1背包问题,可能选择的贪心策略一般有3种。每种贪心策略都是采用多步过程来完成背包的装入,在每一步中,都是利用某种贪心准则来选择将某一件物品装入背包。

        (1)选取价值最大者。   

        贪心策略为:每次从剩余的物品中,选择可以装入背包的价值最大的物品装入背包。这种策略不能保证得到最优解。例如,设C=30,有3个物品A、B、C,W[3]={28,12,12},P[3]={30,20,20}。根据策略,首先选取物品A,接下来就无法再选取了,此时最大价值为30。但是,选取装B和C,最大价值为40,显然更好。

        (2)选取重量最小者。  

        贪心策略为:从剩下的物品中,选择可以装入背包的重量最小的物品装入背包。其想法是通过多装物品来获得最大价值。这种策略同样不能保证得到最优解。例如,设C=30,有3个物品A、B、C,W[3]={13,14,15},P[3]={20,30,40}。根据策略,首先选取物品A,接下来选取B,之后就无法再选取了,此时最大价值为50。但是,选取装B和C,最大价值为70,显然更好。

        (3)选取单位重量价值最大者

        贪心策略为:从剩余物品中,选择可装入背包的P[i]/W[i]值最大的物品装入。这种策略还是不能保证得到最优解。例如,设C=40,有3个物品A、B、C,W[3]={15,20,28},P[3]={15,20,30}。按照策略,首先选取物品C(p[2]/w[2]>1),接下来就无法再选取了,此时最大价值为30。但是,选取装A和B,最大价值为35,显然更好。

        由上面的分析可知,采用贪心法并不一定可以求得最优解。

        背包问题用贪心和搜索求解的效果不佳,其标准的解法是动态规划。

1.编程思路1。

        按每一件物品装包为一个阶段,共分为n个阶段。

      

        (1)建立递推关系

        设f[i][j]为背包容量j,可取物品范围为i、i+1、…、n的最大效益值。例如,f[1][c]的含义是容量为c的背包、可在1~n件物品中选择物品装入背包后所得的最大效益值。

        当0≤j<w[i] 时,物品i不可能装入。最大效益值与f[i+1][j] 相同。

        当j≥w[i] 时,有两个选择:

        1)不装入物品i,这时最大效益值为f[i+1][j] ;

        2)装入物品i,这时已产生效益p[i],背包剩余容量 j−w[i],可以选择物品i+1、…、n来装,最大效益值为f[i+1][j−w[i]] + p[i]。

        期望的最大效益值是两者中的最大者。于是递推关系(或称状态转移方程)如下:

       

        其中w[i]、p[i] 均为正整数,i=1、2、…、n。

        边界条件为: f[n][j]=p[n]    当j≥w[n] 时 (最后1件物品可装包) ;

                               f[n][j]=0        当 j<w[n] 时  (最后1件物品不能装包)。

        所求最大效益即最优值为f[1][c]。

        (2)逆推计算最优值

    for (j=0;j<=c;j++)       //  首先计算边界条件f[n][j] 

       if (j>=w[n])

           f[n][j]=p[n];                              

       else

           f[n][j]=0;

    for(i=n-1;i>=1;i--)      //  逆推计算f[i][j] (i从n-1到1)  

      for(j=0;j<=c;j++)

         if (j>=w[i] && f[i+1][j]<f[i+1][j-w[i]]+p[i])

            f[i][j]= f[i+1][j-w[i]]+p[i];

         else

            f[i][j]=f[i+1][j];

  printf("最优值为%d\n",f[1][c]);

        (3)构造最优解

        若f[i][cw] > f[i+1][cw]  ( i=1、2、…、n−1, cw的初始值为c)

        则x[i]=1; 装载w[i], cw=cw−x[i]*w[i]。

        否则,x[i]=0,不装载w[i]。

        最后,所装载的物品效益之和与最优值比较,决定w[n]是否装载。

2.源程序1及运行结果。

#include <stdio.h>
#define MAXN 500
#define MAXC 50000
int f[MAXN][MAXC];
int  main()
{
    int p[MAXN],w[MAXN];
    int n,c;
    printf("请输入物品的个数 N:");
    scanf("%d",&n);
    printf("请输入背包容量 C:");
    scanf("%d",&c);
    printf("请依次输入每种物品的重量:");
    int i,j;
    for (i=1;i<=n;i++)
       scanf("%d",&w[i]);
    printf("请依次输入每种物品的价值:");
    for (i=1;i<=n;i++)
       scanf("%d",&p[i]);
    for (j=0;j<=c;j++)          //  首先计算边界条件f[n][j]
       if (j>=w[n])
           f[n][j]=p[n];
       else
           f[n][j]=0;
    for (i=n-1;i>=1;i--)        //  逆推计算f[i][j] (i从n-1到1)
      for(j=0;j<=c;j++)
         if (j>=w[i] && f[i+1][j]<f[i+1][j-w[i]]+p[i])
            f[i][j]= f[i+1][j-w[i]]+p[i];
         else
            f[i][j]=f[i+1][j];
    int cw=c;
    printf("背包所装物品如下:\n");
    printf("  i     w(i)    p(i) \n");
    printf("----------------------\n");
    int sp=0,sw=0;
    for (i=1;i<=n-1;i++)     // 以表格形式输出结果
       if (f[i][cw]>f[i+1][cw])
       {
           cw-=w[i];  sw+=w[i]; sp+=p[i];
           printf("%3d %8d %8d\n",i,w[i],p[i]);
       }
    if (f[1][c]-sp==p[n])
    {
         sw+=w[n];sp+=p[n];
         printf("%3d %8d %8d\n",n,w[n],p[n]);
    }
    printf("装载物品重量为 %d ,最大总价值为 %d\n",sw,sp);
    return 0;
}

编译并执行以上程序,可得到如下所示的结果。

请输入 n 值:6

请输入背包容量:60

请依次输入每种物品的重量:15 17 20 12 9 14

请依次输入每种物品的价值:32 37 46 26 21 30

背包所装物品如下:

  i     w(i)    p(i)

----------------------

  2      17      37

  3      20      46

  5       9      21

  6      14      30

装载物品重量为 60 , 最大总价值为 134

3.编程思路2。

        思路1中采用逆推的方法来求解的。实际上在应用动态规划时,还可以顺推求解。

        (1)建立递推关系

        设f[i][j]为背包容量j,可取物品范围为1、2、…、i的最大效益值。

        当0≤j<w[i] 时,物品i不可能装入。最大效益值与f[i−1][j] 相同。

        当j≥w[i] 时,有两种选择:

        1)不装入物品i,这时最大效益值为f[i−1][j] ;

        2)装入物品i,这时已产生效益p[i],背包剩余容量j−w[i],可以选择物品1、2、…、i−1来装,最大效益值为f[i−1][j−w[i]]+p[i] 。

        期望的最大效益值是两者中的最大者。于是有递推关系

        

        边界条件为:  f[1][ j]= p[1]    当 j≥w[1] 时;

                               f[1][ j] = 0      当j<w[1]  时。

        所求最大效益即最优值为f[n][c]。

        (2)顺推计算最优值

    for(j=0;j<=c;j++)                  //  首先计算边界条件f[1][j]

       if (j>=w[1] ) f[1][j]=p[1];               

       else  f[1][j]=0;

    for (i=2;i<=n;i++)                   //  顺推计算f[i][j]  (i从2到n)  

       for (j=0;j<=c;j++)

          if(j>=w[i] && f[i-1][j]<f[i-1][j-w[i]]+p[i])

              f[i][j]= f[i-1][j-w[i]]+p[i];

          else  f[i][j]=f[i-1][j];

  printf("最优值为%d\n",f[n][c]);

        (3)构造最优解

        若f[i][cw] > f[i-1][cw]  ( i=1、2、…、n−1, cw的初始值为c)

        则x[i]=1; 装载w[i], cw=cw−x[i]*w[i]。

        否则,x[i]=0,不装载w[i]。

        最后,所装载的物品效益之和与最优值比较,决定w[1]是否装载。

4.源程序2及运行结果。

#include <stdio.h>
#define MAXN 500
#define MAXC 50000
int f[MAXN][MAXC];
int  main()
{
    int p[MAXN],w[MAXN];
    int n,c;
    printf("请输入物品的个数 N:");
    scanf("%d",&n);
    printf("请输入背包容量 C:");
    scanf("%d",&c);
    printf("请依次输入每种物品的重量:");
    int i,j;
    for (i=1;i<=n;i++)
       scanf("%d",&w[i]);
    printf("请依次输入每种物品的价值:");
    for (i=1;i<=n;i++)
       scanf("%d",&p[i]);
    for(j=0;j<=c;j++)               //  首先计算边界条件f[1][j]
       if(j>=w[1] ) f[1][j]=p[1];
       else  f[1][j]=0;
    for(i=2;i<=n;i++)              //  顺推计算f[i][j]  (i从2到n)
       for(j=0;j<=c;j++)
          if(j>=w[i] && f[i-1][j]<f[i-1][j-w[i]]+p[i])
              f[i][j]= f[i-1][j-w[i]]+p[i];
          else  f[i][j]=f[i-1][j];
    int cw=c;
    printf("背包所装物品如下:\n");
    printf("  i     w(i)    p(i) \n");
    printf("----------------------\n");
    int sp=0,sw=0;
    for (i=n;i>=2;i--)     // 以表格形式输出结果
       if(f[i][cw]>f[i-1][cw])
       {
           cw-=w[i]; sw+=w[i]; sp+=p[i];
           printf("%3d %8d %8d\n",i,w[i],p[i]);
       }
    if(f[n][c]-sp==p[1])
    {
        sw+=w[1];sp+=p[1];
        printf("%3d %8d %8d\n",1,w[1],p[1]);
    }
    printf("装载物品重量为 %d ,最大总价值为 %d\n",sw,sp);
    return 0;
}

编译并执行以上程序,得到如下所示的结果。

请输入 n 值:6

请输入背包容量:60

请依次输入每种物品的重量:15 17 20 12 9 14

请依次输入每种物品的价值:32 37 46 26 21 30

背包所装物品如下:

  i     w(i)    p(i)

----------------------

  6      14      30

  5       9      21

  3      20      46

  2      17      37

装载物品重量为 60 , 最大总价值为 134

5.编程思路3。

        仔细分析编程思路2及其源程序可发现,第 i 件物品的选取决策只与第i-1件有关,与其他无关,即f[i][j]只与f[i-1][j]有关,f[i-2][*]、f[i-3][*]、…这些存储空间的数据是不会再使用的,空间就浪费了。如果采用一维数组,新的状态直接覆盖在旧的上面,迭代使用,就可把空间复杂度从O(N*C)优化为O(C)。

        (1)建立递推关系

        设f[j]为背包装载的物品容量不超过j时,可获得的最大效益值。

        当0≤j<w[i] 时,物品i不可能装入。f[j]的值不改变,无需处理。

        当j≥w[i] 时,有两种选择:

        1)不装入物品i,这时最大效益值为f[j] ;

        2)装入物品i,这时会产生效益p[i],这实际上是在背包容量为j−w[i]的背包中装入物品i,最大效益值为f[j−w[i]]+p[i] 。

        期望的最大效益值是两者中的最大者。于是有递推关系

             f[j]=max(f[j],f[j-w[i]]+p[i])

        所求最大效益即最优值为f[c]。

        (2)逆推计算最优值。

        在前面使用二维数组时,为了计算最优值,采用顺推和逆推的方法都可以,因为使用二维数组时,中间的所有状态都保留了下来。

        但是,使用一维数组时,究竟是使用顺推还是逆推,就需要看具体的问题了。

        由于本题中每个物品要么不装入,要么只能装入1次(每个物品只有1件)。因此,只能采用逆推的方法计算最优值。写成如下的循环。

    for (i=1;i<=n;i++)        // 对每个物品进行处理

       for(j=c;j>=w[i];j--)    //  逆推计算f[j]

          f[j]=max(f[j], f[j-w[i]]+p[i]);

        为什么要逆序枚举计算呢?

        如果是正序枚举的话,循环写成

    for (i=1;i<=n;i++)         // 对每个物品进行处理

       for(j=w[i];j<=c;j++)    //  正序(顺序)计算f[j]

          f[j]=max(f[j], f[j-w[i]]+p[i]);

        下面我们用简单的测试数据作为示例进行详细说明。

        设背包容量C=8,有两件物品,重量分别为w1=2,w2=3;价值分别为p1=3,p2=4。

        初始时,f[0]~f[8]全部为0,背包没有装入任何物品,其装入价值显然为0。

        采用正序枚举时,当i=1,处理第1件物品,依次的计算过程如下:

        f[2]=max { f[2], f[2-w1]+p1 } =max { 0, 0+3} =3

        f[3]=max { f[3], f[3-w1]+p1 } =max { 0, 0+3} =3

        f[4]=max { f[4], f[4-w1]+p1 } =max { 0, f[2]+3} = 6 (这里实际就出问题了,因为第1件物品只有1件,在计算f[2]时装入了1次,这里又装入1次,不可能的)

        f[5]=max { f[5], f[5-w1]+p1 } =max { 0, f[3]+3} =6  (同上,第1件物品又装入了1次)

        f[6]=max { f[6], f[6-w1]+p1 } =max { 0, f[4]+3} =9  (第1件物品装入了3次)

        f[7]=max { f[7], f[7-w1]+p1 } =max { 0, f[5]+3} =9  (同上,第1件物品装入了3次)

        f[8]=max { f[8], f[8-w1]+p1 } =max { 0, f[6]+3} =12 (第1件物品装入了4次)

        当i=2,处理第2件物品,依次的计算过程如下:

        f[3]=max { f[3], f[3-w2]+p2 } =max { 3, f[0]+4} =4   (第2件物品装入了1次)

        f[4]=max { f[4], f[4-w2]+p2 } =max { 6, f[1]+4} =6   (实际是第1件物品装入2次)

        f[5]=max { f[5], f[5-w2]+p2 } =max { 6, f[2]+4} =7   (第1件物品装入1次,第2件物品装入1次)

        f[6]=max { f[6], f[6-w2]+p2 } =max { 9, f[3]+4} =9   (实际是第1件物品装入3次)

        f[7]=max { f[7], f[7-w2]+p2 } =max { 9, f[4]+4} =10  (实际是第1件物品装入2次,第2件物品装入1次)

        f[8]=max { f[8], f[8-w2]+p2 } =max { 12, f[5]+4} =12 (实际是第1件物品装入4次)

        循环处理结束后,最优值f[8]=12,这显然是不对的,因为只有2件物品,全部装入背包,最大价值也只有3+4=7。

        如果采用逆序枚举,我们再来分析循环的处理过程。

        当i=1,处理第1件物品,依次的计算过程如下:

        f[8]=max { f[8], f[8-w1]+p1 } =max { 0, f[6]+3} =3  (物品1装入背包,背包容量为8)

        f[7]=max { f[7], f[7-w1]+p1 } =max { 0, f[5]+3} =3  (物品1装入背包,背包容量为7)

        f[6]=max { f[6], f[6-w1]+p1 } =max { 0, f[4]+3} =3  (物品1装入背包,背包容量为6)

        f[5]=max { f[5], f[5-w1]+p1 } =max { 0, f[3]+3} =3  (物品1装入背包,背包容量为5)

        f[4]=max { f[4], f[4-w1]+p1 } =max { 0, f[2]+3} =3  (物品1装入背包,背包容量为4)

        f[3]=max { f[3], f[3-w1]+p1 } =max { 0, f[1]+3} =3  (物品1装入背包,背包容量为3)

        f[2]=max { f[2], f[2-w1]+p1 } =max { 0, f[0]+3} =3  (物品1装入背包,背包容量为2)

        也就是,物品1的重量为2,可装入背包容量为2~8的背包中,得到最大价值为3。

        当i=2,处理第2件物品,依次的计算过程如下:

        f[8]=max { f[8], f[8-w2]+p2 } =max { 3, f[5]+4} =7  (实际是物品1和物品2装入背包)

        f[7]=max { f[7], f[7-w2]+p2 } =max { 3, f[4]+4} =7  (实际是物品1和物品2装入背包)

        f[6]=max { f[6], f[6-w2]+p2 } =max { 3, f[3]+4} =7  (实际是物品1和物品2装入背包)

        f[5]=max { f[5], f[5-w2]+p2 } =max { 3, f[2]+4} =7  (实际是物品1和物品2装入背包)

        f[4]=max { f[4], f[4-w2]+p2 } =max { 3, f[1]+4} =4  (实际是物品2装入背包)

        f[3]=max { f[3], f[3-w2]+p2 } =max { 3, f[0]+4} =4  (实际是物品2装入背包)

        由上面的计算过程知,逆推计算时,每个物品若装入背包,最多装入1次。

        由上面的分析大家也可产生一个印象,若每种物品只有1件,1个物品装入背包最多只能装入1次,则采用逆序递推的方法计算最优值,这也是0/1背包的基本模式;若每种物品有无数件,可以不限次数地装入背包中,则采用顺序(正序)递推的方法计算最优值,这也是完全背包的基本模式。对于0/1背包和完全背包,后面会进行更详细地阐述。

        (3)构造最优解。

        如果要求输出某个最优解,需要记录每个状态的最优值是由状态转移方程的哪一项推出来的。

        如果我们知道了当前状态是由哪一个状态推出来的,就能容易的输出某个最优解了。

        为此,最简单的方法是定义数组g[n][C],其中g[i][j]就记录第i件物品在加入背包时,其状态f[j]是由状态转移方程f[j]=max(f[j], f[j-w[i]]+p[i])哪一项推出。若第i件物品加入了背包,即f[j]= f[j-w[i]]+p[i],置g[i][j]=1;若第i件物品不加入背包,即f[j]=f[j],置g[i][j]=0。

        改写上面的逆推计算最优值循环如下。

    for (i=1;i<=n;i++)        // 对每个物品进行处理

       for(j=c;j>=w[i];j--)    //  逆推计算f[j]

       {

           if (f[j]<f[j-w[i]]+p[i])

           {

               f[j]=f[j-w[i]]+p[i];

               g[i][j]=1;     // 选择第i件物品装入

           }

           else

               g[i][j]=0;    // 不选择第i件物品装入

       }

        由此,可用如下循环输出某个最优解。

int T=c;

for (i=n;i>=1;i--)

{

    if  (g[i][T])

    {

        printf("used %d",i);

        T-=w[i];       //减去物品i的重量

    }

}

        当然,为了输出某个最优解,我们又定义了一个二维数组,这样我们采用一维数组进行优化的目的并没有达到,还不如直接像编程思路1或编程思路2那样,直接采用二维数组保存各状态,再构造出最优解。

但是,如果只要求得到最优值,而无需输出某个最优解,采用一维数组解决问题还是非常有意义的。

6.源程序3及运行结果。

#include <stdio.h>
#define MAXN 500
#define MAXC 50000
int f[MAXC]={0};
int g[MAXN][MAXC];
int  main()
{
    int n,c;
    printf("请输入物品的个数 N:");
    scanf("%d",&n);
    printf("请输入背包容量 C:");
    scanf("%d",&c);
    printf("请依次输入每种物品的重量:");
    int p[MAXN],w[MAXN];
    int i,j;
    for (i=1;i<=n;i++)
       scanf("%d",&w[i]);
    printf("请依次输入每种物品的价值:");
    for (i=1;i<=n;i++)
       scanf("%d",&p[i]);
    for (i=1;i<=n;i++)        // 对每个物品进行处理
       for(j=c;j>=w[i];j--)   //  逆推计算f[j]
       {
           if (f[j]<f[j-w[i]]+p[i])
           {
               f[j]=f[j-w[i]]+p[i];
               g[i][j]=1;     // 选择第i件物品装入
           }
           else
               g[i][j]=0;    // 不选择第i件物品装入
       }
    printf("背包所装物品如下:\n");
    printf("  i     w(i)    p(i) \n");
    printf("----------------------\n");
    int t=c;
    for (i=n;i>=1;i--)
    {
        if (g[i][t])
        {
            printf("%3d %8d %8d\n",i,w[i],p[i]);
            t-=w[i];         // 减去物品i的重量
        }
    }
    printf("装载物品重量为 %d ,最大总价值为 %d\n",c-t,f[c]);
    return 0;
}

编译并执行以上程序,得到如下所示的结果。

请输入物品的个数 N:6

请输入背包容量 C:60

请依次输入每种物品的重量:15 17 20 12 9 14

请依次输入每种物品的价值:32 37 46 26 21 30

背包所装物品如下:

  i     w(i)    p(i)

----------------------

  6       14       30

  5        9       21

  3       20       46

  2       17       37

装载物品重量为 60 ,最大总价值为 134

posted on 2022-03-29 19:15  aTeacher  阅读(1062)  评论(0编辑  收藏  举报