0-1背包问题动态规划详解

最近开始学习背包问题,在此做些笔记,先学习最简单的0-1背包问题。

0-1背包问题介绍:

一,背包问题基本解决

       有一个背包可以存放M斤物品,有N件物品(每件物品只有1件),他们重量分别是w1,w2,w3..........,他们价值分别是p1,p2,p3...............。问怎么装载物品,使背包装的载物品价值最大?

       举例说明:

       背包装10斤物品,有3件物品,重量分别是3斤,4斤,5斤,价值分别是4,5,6;

可以画一个矩阵,行号0,1,2,3,行号0表示0件物品可放入背包情况下,背包装载最大价值,行号1表示只有第一件物品可以放入背包情况下,背包装载最大价值,行号2表示只有第1件和第2件可以放入背包情况下,背包装载最大价值,......

      列号0,1,2,3.............10表示背包剩余可用容量,列号0表示背包已满时候,剩余容量为0时候,还可以装载的最大价值;列号1表示背包剩余容量为1时候,还可以装载的最大价值,......

      当行号是3,列号是10时候,价值最大,即c[3][10]值最大.

       行号是0,表示有0件物品可放入背包,此时背包装载0件物品,因此价值都为0,即c[0][0]=c[0][1]=.....c[0][10]=0;

       行号是1,表示第一件物品可以放入背包,第一件物品重量是3,当列号为0,1,2时候,一件物品也放不下,背包装载价值是0,当列号3,4,5...........10可以放下,但是只有一件物品,因此最多获取价值是4,即c[1][3]=c[1][4]..........c[1][10] = 4;

       行号是2,表示第一件和第二件物品可以放入背包,第一件物品重3斤,第二件重4斤,当列号0,1,2时候,一件物品也放不下,背包装载价值是0,当列号为3时候,可以放下第一件,但是放不下第二件,价值是4,列号为4时候,可以放下第一件也可以放下第二件,此时分为两种情况,放第二件或不放第二件,取两者最大值即可。放第二件,剩余容量是0,此时价值是p[2]+容量0,行号是1时最大值,即p[2]+c[1][0]=5,不放第二件,则是剩余容量是4,行号是1时,最大值,即c[1][4] = 4,则5>4,我们存放第二件,此时背包装载最大价值是5,当列号为5,6时候,价值5,当列号为7时候,max{不放第二件,放第二件},不放第二件,即列号为7,行号为1最大值,c[1][7]=4;放第二件,为:p[2]+列号为7-w[2]=7-4=3,行号为1时最大值,为:p[2]+c[1][3]=5+4=9,9>4,我们选择存放第二件,此时装载了两件物品,列号8,9,10也是9.

       当行号是2时,我们可以得出规律:最大值=max{存放第二件,不存放第二件}=max{p[2]+c[1][列号-w[2]],c[1][列号]},而列号就是从1到背包容量V之间某个值。

       当行号是1时,我们可以得出规律:最大值=max{存放第一件,不存放第一件}=max{p[1]+c[0][列号-w[1]],c[0][列号]}=max{p[1],0},当列号>=w[1],最大值p[1],否则0。

       我们由此得出一个疑问:为什么列号是0,1,2....10,可以这样(列号=10不变)吗?因为我们只求背包剩余容量为10时,装载价值最大值。

答案是不可以的,比如求当行号是3,列号是10最大值,即最大值=c[3][10] = max{p[3]+c[2][5],c[2][10]},欲求c[3][10]必须知道c[2][5]和c[2][10],而c[2][5] = max{p[2] + c[1][1],c[1][5]},欲求c[2][5]必须求c[1][1]c[1][5],我们求c[i][10]时候会用到c[i][j],此时0<=j<=10,因此我们必须从行号是0开始,把列号0......10之间,所有c[i][j]求出来,因为后面要用到,这是一个不断递推过程。

       因此,得出结论:

       将前i件物品放入背包中,求背包装载最大价值问题。只考虑第i件物品策略(第i件放还是不放),如果不放第i件物品,求前 i-1 件物品放入容量背包,背包装载最大价值问题,最大价值为c[i-1][v];如果放第i件物品,求 i-1 件物品放入容量 v-w[i] 容量背包,背包装载最大价值问题,最大价值为p[i] +c[i-1][v-w[i-1]];

因此c[i][j] = max{c[i-1][v],p[i]+c[i-1][v-w[i-1]]};

       因此我们得出核心代码:

for (int i = 1; i <= N; i++){
    for(int j = 1; j <= M; j++){
        if (c[i][j] >= w[i]){
            if (c[i-1][j] > p[i]+c[i-1][j-w[i]]){
                c[i][j] = c[i-1][j];
            }else{
                c[i][j] = p[i] + c[i-1][j-w[i]];
            }
        }else{
            c[i][j] = c[i-1][j];
        }
    }
}

下面是用C++简单实现程序(VS2008通过):

 1 #include <cstdio>
 2 const int  K = 100;
 3 
 4 
 5 /*
 6 输入格式:
 7 10 3    //M=10,N=3
 8 3 4     //w[1]=3,p[1]=4
 9 4 5     //w[2]=4,p[2]=5
10 5 6     //w[3]=5,p[3]=6
11 */
12 
13 int max(int a,int b){
14     return a>b?a:b;
15 }
16 int main(){
17     int M,N,w[K],p[K],i,j,c[K][K];
18     scanf("%d%d",&M,&N);
19     for (i = 1;i <= N;i++)
20     {
21         scanf("%d%d",&w[i],&p[i]);
22     }
23     for (i = 0;i <= N;i++)
24     {
25         for (j = 0;j <= M;j++)
26         {
27             c[i][j] = 0;
28         }
29     }
30 
31     for (i = 1;i <= N;i++)
32     {
33         for (j = 1;j <= M;j++)
34         {
35             if (w[i] <= j)
36             {
37                 c[i][j] = max(c[i-1][j],c[i-1][j-w[i]]+p[i]);   //方式1
38 /*                if (c[i-1][j] > c[i-1][j-w[i]]+p[i])    //方式2
39                 {
40                     c[i][j] = c[i-1][j];
41                 }else{
42                     c[i][j] = c[i-1][j-w[i]]+p[i];
43                 }
44 */
45             }else{
46                 c[i][j] = c[i-1][j];
47             }
48         }
49     }
50 
51     printf("%d\n",c[N][M]);
52     return 0;
53 }
View Code

 运行结果:

二,背包问题优化解决

          在以上解决方案中,所用时间复杂度是O(M*N),空间复杂度是O(M*N),用到了一个M*N的数组。如果仔细观察之后,发现可以用一维数组代替二维数组。

          当行号为0时,数组c[0][0]=c[0][1]=...................=c[0][10] = 0;

          当行号为1时,根据c[i][j] = max{c[i-1][j],p[i]+c[i-1][j-w[i]]},得c[1][j]=max{c[0][j],p[1]+c[0][j- w[1]]}。即c[1][0,1,2.......,9,10]由c[0][0,1,2.......,9,10]得到。

          因此我们可以利用a[j]表示c[0][j],用b[j]表示c[1][j],首先b[0]=a[0] = 0,b[1]=a[1]=0,.........b[10]=a[10]=0。将数组b和数组a值设为一样。然后如果w[i]< j,再根据公式b[j]=max{a[j],p[1]+a[j-w[1]]},重新确定b[j]的值,如果w[i] >= j,则不变。

          但是我们仔细观察一下次公式b[j]=max{a[j],p[1]+a[j-w[1]]},发现b[j]得值由a[k],p[1]和w[1]所得,而k的 取值范围是0<=k<=j,即b[j]由数组a中下标小于j的元素得到。因此我们可以将j从大到小使用来得到b[j],当i = 0时候,a[0] =  a[1] = a[2]=a[3]=..............=a[10]=0,当i=1时,先设定 b[0] = a[0],b[1]=a[1],b[2]=a[2],b[3]=a[3],..............=b[10]=a[10],我们先计算 b[10],b[10]=max{a[10],p[1]+a[10-w[1]]}=max{a[10],4+a[7]},此处 p[1]=4,w[1]=3;因为a[10]=b[10],a[7]=b[7],因此b[10]=max{b[10],4+b[7]}=4,至此数组b和 数组a只有第10个元素值不想等,其他元素都相等;b[9]=max{a[9],p[1]+a[9- w[1]]}=max{a[9],4+a[6]},a[9]=b[9],a[6]=b[6],因此b[9]=max{b[9],b[6]+4}=4,至此 数组b和数组a只有第9,10个元素值不想等,其他元素都相等;因此求的 b[8]=max{b[8],4+b[5]},b[7]=max{b[7],4+b[4]}.............b[3]=max{b[3],4+b[0]}。 在求解过程中,我们发现只要按数组b元素下标从大到小求解,就可以用自己求的自己的值。

至此我们可以利用一个一维数组代替二维数组。

核心代码:

for (int i = 1; i <= N; i++){
    for(int j = M; j > 0; j--){
        if (c[i] <= j){
            if (c[j] > p[i]+c[j-w[i]]){
                c[j] = c[j];
            }else{
                c[j] = p[i] + c[j-w[i]];
            }
        }
    }
}

下面是用C++简单实现程序(VS2008通过):

 1 #include <cstdio>
 2 const int  K = 100;
 3 
 4 
 5 /*
 6 输入格式:
 7 10 3    //M=10,N=3
 8 3 4     //w[1]=3,p[1]=4
 9 4 5     //w[2]=4,p[2]=5
10 5 6     //w[3]=5,p[3]=6
11 */
12 
13 int max(int a,int b){
14     return a>b?a:b;
15 }
16 
17 int main(){
18     int M,N,w[K],p[K],i,j,f[K] = {0};
19     scanf("%d%d",&M,&N);
20     for (i = 1;i <= N;i++)
21     {
22         scanf("%d%d",&w[i],&p[i]);
23     }
24 
25     for (i = 1;i <= N;i++)
26     {
27         for (j = M;j > 0;j--)
28         {
29             if (w[i] <= j)
30             {
31                 f[j] = max(f[j],p[i]+f[j-w[i]]);
32             }
33         }
34     }
35 
36     printf("%d\n",f[M]);
37     return 0;
38 }
View Code

运行结果:

posted on 2013-05-19 18:59  张三的哥哥  阅读(4873)  评论(0编辑  收藏  举报