0-1背包和完全背包 的完整讲解版 包含 一维数组实现 和二维数组实现题目

来源:互联网 发布:天山股份历史行情数据 编辑:程序博客网 时间:2024/05/16 18:40

转自:http://www.360doc.com/content/13/0705/12/13049620_297797824.shtml


//N件物品和一个容量为V的背包。第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

//基本思路

//这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

//用子问题定义状态:即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件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[i-1][v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]

//这时候我们可以用二维数组进行做了

/*任务:计算完全背包问题的最大价值

Sample Input

10 4

2 1

3 3

4 5

1 9

Sample Output

90

0 0 0 10

*/

#include<stdio.h>

#include<string.h>

#include <iostream>

using namespace std;

#include <iomanip>

 

 

 

void print(int nint m);

 

void printNumber(int nint m);

 

int c[20][1000];//c[k][y]为只允许装前k种物品,背包总重量不超过y的最大价值

int inumber[21][1000];//inumber[k][u]为只允许装前K种物品, 背包总重量不超过u时得到最大价值时使用的背包的最大标号

int w[21],p[21];

 

int knapsack(int m,int n)

{

 int i,j;

//    for(i=1;i<n+1;i++)

//        cin>>w[i]>>p[i];

    memset(c,0,sizeof(c));

 memset(inumber,0,sizeof(inumber));

 for(j=1;j<m+1;j++)//设置边界值

 {

  c[1][j]=j/w[1]*p[1];

 }

 printn,  m);

 printNumber(n,m);

    for(i=1;i<n+1;i++)

 {

  for(j=1;j<m+1;j++)

  {

   if(j >= w[i])

   {

 

//参考 金矿模型来说的    完全背包的判断如下:p[i]+c[i][j-w[i],解析为:对第i个矿进行开采,其价值为p[i],用去了w[i]个人,

//完全背包问题中每件物品有无限个,然后仍然对前i个矿进行开采,已知现在还可以用的人为 j-w[i],c[i][j-w[i]]为其最大值

// c[i-1][j]:表示如果对第i个矿不开采,那么对前i-1个矿进行开采时,可以用的人数仍然是j个,则c[i-1][j]为其最大值

//,具体代码表示如下

 //   if(p[i]+c[i][j-w[i]]>=c[i-1][j])

    {//0-1背包相比只是将c[i-1][j-w[i]]写成了c[i][j-w[i]],因为完全背包问题中每件物品有无限个

//     c[i][j]=p[i]+c[i][j-w[i]];

//     inumber[i][j]=i;//记录对第i个金矿进行了开采,在使用inumber[i][j]的时候,只需统计减去w[i]*nn1,2..n

     //后,在inumber中仍然是 i的个数,那个就是 i个金矿被开发的次数

   

//    }

//参考 金矿模型来说的   

//    01背包的判断如下:p[i]+c[i-1][j-w[i],解析为:对第i个矿进行开采,其价值为p[i],用去了w[i]个人,

//    ,然后对前i-1个矿进行开采,已知现在还可以用的人为 j-w[i],c[i-1][j-w[i]]为其最大值

// c[i-1][j]:表示如果对第i个矿不开采,那么对前i-1个矿进行开采时,可以用的人数仍然是j个,则c[i-1][j]为其最大值

//以上是参考 金矿模型来说的,具体代码表示如下

    if(p[i]+c[i-1][j-w[i]]>=c[i-1][j])

    {

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

     inumber[i][j]=i;

   }

   

                else

    {

     c[i][j]=c[i-1][j];

     inumber[i][j]=inumber[i-1][j];//inumber[][]有两个作用,一是 做备忘,二是直接使用前面的备忘

                                   //此处就是 二的作用,这个不同于金矿模型之处在于,

     //金矿模型的 maxGold[max_n][max_people],将本例的 c[20][1000] inumber[21][1000]的功能和二为一

     //max_people比真实的people多一个,这样造成maxGold多一列,就是灵活运用这多的一列来来完成inumber[][]功能

     //这一列做备忘时,只是记录 可以开采出来的最大金子价值,二是要判断一下是否已经备忘过,如果有则使用,不是

     //直接使用,是经过判断的还有就是 多的那一列,用于判断01背包的使用情况时,只需经过上下行的数值的不同

     //就可以判断出使用情况,但不适用于完全背包,对于完全背包,最多只能求出金子的最大价值,而inumber[][]

     //是可以求出使用情况的

    

    }

   }

   else

   {

    c[i][j]=c[i-1][j];

       inumber[i][j]=inumber[i-1][j];

   

   }

  }

  printn,  m);

  printNumber(n,m);

 }

 return(c[n][m]);                    

}

 

void print(int nint m)

{

    cout<<endl<< "void print() begin!!!!" <<endl;

    cout << "备忘录的内容:" <<endl<<endl;

 for(int i=1;i<n+1;i++)

 {

  for(int j=1;j<m+1;j++)

  {

  

            cout <<setw(4)<<  c[i][j] <<"" ;

           

        }

        cout << endl;

    }

    cout<<endl<< "void print() end end end !!!!" <<endl<<endl;

}

 

void printNumber(int nint m)

{

    cout<<endl<< "void printNumber() begin!!!!" <<endl;

    cout << "背包的标号" <<endl<<endl;

 for(int i=1;i<n+1;i++)

 {

  for(int j=1;j<m+1;j++)

  {

  

            cout <<setw(4)<<  inumber[i][j] <<"" ;

           

        }

        cout << endl;

    }

    cout<<endl<< "void printNumber() end end end !!!!" <<endl<<endl;

}

 

void trackSolution(int mint n)

{

 cout<<endl<< "void trackSolution(int m, int n) begin!!!!" <<endl;

 int x[21];

 int y = m;

 int j = n;

 memset(x, 0, sizeof(x));

 /************************************************************************/

 /* 难点:

 第一轮  j=j1= inumber[][]最后一行最后一列的那个值,由于inumber[][]的值记录了对那个金矿进行了开采,所以此处的j

        就表示了对第j坐金矿进行了开采,原来有y=y_max=m个人,由于开本金矿,要用w[j]个人,还剩y = y_max -  w[j]个人,

     然后用这y = y_max -  w[j]个人,仍然对这包括第j个金矿的前j个金矿进行开采,

     如果inumber[j][y] ==inumber[j][y_max -  w[j]]== j,表示第j座金矿又被开采此一次,

     那么此时还剩下 y=y_max -  w[j] - w[j],然后再次判断是否 inumber[j][y] ==inumber[j1][y_max - w[j]- w[j]]== j 

     如果仍然等于,则继续内层循环,序列数组x[j]++

     否则 表示对第j个金矿的开采次数已经完全记录了下来,此时的j的值已经发生了变化,下面的记录已经与第j座金矿无关了,

     如果此时的 inumber[j][y] 的值 ==0,表示 所有的金矿都已经统计了,表示inumber[j][y] ==0是数组边界,直接跳出外边循环,

     否则 进行第二轮

 第二轮 此处对inumber[][]是从最后一行进行倒序统计的,所以第二轮的j=j2肯定小于第一轮的j=j1,

        j=j2=inumber[j2][y_max - w[j1]*x[j1]],所以此处的j 就表示了对第j2坐金矿进行了开采,

        原来有y=y2=y_max - w[j1]*x[j1] 个人,由于开本金矿,要用w[j2]个人,还剩y = y2 -  w[j2]个人,

     然后用这y = y2 -  w[j2]个人,仍然对这包括第j2个金矿的前j2个金矿进行开采,后面的逻辑同于第一轮的逻辑,

     总之,如果第二轮完不成,就进行三轮,依次类推。

 

 */

 /************************************************************************/

 while(true)

 {

  j = inumber[j][y];

  x[j] = 1;

  y = y - w[j];

  while(inumber[j][y] == j)

  {

   y = y - w[j];

   x[j]++;

  }

  printf("trackSolution()中的inumber[i][j] \n");

  printNumber(n,m);

  if(!inumber[j][y]) break;

 }

 

 printf("最大价值方案中各个物品的个数为(物品标号从1n)");

 for(j = 1; j <= nj++)

 {

  printf("%d "x[j]);

 }

 printf("\n");

 

 

 

 cout<<endl<< "void trackSolution(int m, int n) end end end!!!!" <<endl;

}

int main()

{

 freopen("beibao.in","r",stdin);

 freopen("WQbeibao.out","w",stdout);

 int m,n;

    cin>>m>>n;

    int i,j;

   

    for(i=1;i<n+1;i++)

        cin>>w[i]>>p[i];

  printf("最大价值为%d\n",knapsack(m,n));

  trackSolution(mn);

 

 return 0;

}

//beibao.in的内容如下:

//20 5

//17 92

//9 22

//4 80

//11 240

//19 90

 

 

//但是我们为了以后解决更加复杂的背包 必须学会用一维数组解决它

//

//先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1..N,每次算出来二维数组f[i][0..V]的所有值。那么,如果只用一个数组f[0..V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[i][v]呢?f[i][v]是由f[i-1][v]f[i-1][v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]f[i-1][v-c[i]]的值呢?事实上,这要求在每次主循环中我们以v=V..0的顺序推f[v],这样才能保证推f[v]f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值

//

//首先想想为什么01背包中要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[v]是由状态f[v-c]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个没有已经选入第i件物品的子结果f[v-c[i]]。

//

//而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[v-c],所以就可以并且必须采用v=0..V的顺序循环。这就是这个简单的程序为何成立的道理。

//

//)。

//

//如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]f[i][v-c[i]]推知

//

//

//

//(因为假设逆序,对于01背包来说,n=5,当 i=2v=V..0=10.0

//此时f[10]已经有值,其值是i=1时求出的,

//f[10-c[2]]也有值,其值也是i=1时求出的;

//

//但是如果是顺序的话,

//那么当 i=2v=0.V=0.10,此时f[10-c[2]]一定在f[10]前求出值,

//也就是说,在求f[10]时,

//f[10]=maxf[10-c[2]]i=2时的求出值,f[10] 有值,其值是i=1时求出的),

//

//这样的结果不是想要的结果,

//那么有人问为什么用二维数组时,可以顺序求解呢,那是因为用二维数组时,,f[i][v-c[i]],f[i-1][v-c[i]],

//f[i][v],f[i-1][v]都分别存储在不同的地方,只需通过i和i-1来控制就可以取到需要的值,

//而在一维数组中f[i][v-c[i]],f[i-1][v-c[i]],他们最终都是在f [v-c[i]]这一个位置上,是会覆盖的,f[i][v],f[i-1][v]同此理。

//与本题意不符,但它却是另一个重要的背包问题完全背包最简捷的解决方案,故学习只用一维数组解01背包问题是十分必要的。

//伪代码如下:

//for i=1..N

//    for v=V..0

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

//画个图给大家演示下

//也就是说此时的f[v],f[v-c[i]] 是前面的

//假设体积是10 

//

//

//                   背包体积----->>>>>

//价值 大小  1  2  3  4  5  6  7  8  9  10      

//   2     1     2  2  2  2  2  2  2  2  2  2      品  (第一决策阶段)

//  1      2     2  2  3  3  3  3  3  3  3  3      (第二决策阶段)

//  5      3     2  2  5  7  7  8  8  8  8  8      

//  4      4     2  2  5  7  7  8  8  8  8  12      

//  3          

// 上表的理解很重要! 每一行代表每个决策阶段!首先,看“背包体积”,当体积为1时,看物品的”大小“,第一决策阶段对应的物品能装下,体积是1,

//价值是2,在表中”背包体积“和”大小“交集的格子填2;这样,在下面更加大的”背包体积“也能装的下"大小"为1的物品,那在第一行(第一决策阶段)

//的剩下的格子也填2;那么到了第二个决策阶段,这里有个重点,就是程序在执行的时候,会先初始化整个一行,是这样初始化的:就是要填的那个

//格子的相邻左和上比较,较大者初始化将要填的格子;然后到了真正要填的时候,看对应决策阶段的物品”大小“和对应的”背包体积“,如果”背包体积“

//小于物品”大小“,那么,格子的最终值是前面默认的,不要改;如果”背包体积“大于物品”大小“,那么将”背包体积“减去物品”大小“所得的值(视作”背

//包体积“)对应的上一阶段的最优解(已填的格子)(在之前的计算已经得出)加上现在的物品”大小“所对应的”价值“,这个值与之前默认的值比较,

//谁较大,谁就是最终的格子的值。    比如:第三决策阶段,第6列的格子的最终值8是怎么来的呢?是这样的,首先初始化默认的值,将相邻的左边7

//和上边3比较,7较大,那么,这个格子的默认初始值就是7。然后到了填这个格子,对应的”背包体积“是6,物品”大小“的3。6>3,那么,6-3=3,这

//个3”背包体积“所对应的上一阶段的最优解为3,然后5加上物品”大小“3本身对应的价值5,即5+3=8,而8>7(默认),所以,最终的最优解为8,所以这

//个格子填8。

//可以看出 f[v]=max{f[v],f[v-c[i]]+w[i]};

//V是从大到小的

//其中的f[v],f[v-c[i]]是前面的值  比较的结果赋值给f[v]

//比如说当i=5的时候   f[v]>f[v-5]注意两者都是i=4时候的值得出的结果12 赋值到新的f[v] 但是此时的f[v]i=5

//当把最后一行改写成55 6的时候    我们可以看出 f[v-6]=f[4]+55  大于f[v] 所以我们可以得出新的f[v]=f[4]+55这样结果就是取的体积为1 2 6   

//代码如下//一维数组实现背包问题

#include <stdio.h>

#include <string.h>

#define N 3500

#define M 13000

int d[N],w[N],val[M],n,W;

 

void knapsack()

{

    int i,j;

    memset(val,0,sizeof(val));

    for(i=1;i<=n;i++)

        for(j=W;j>=1;j--)

        {//当前背包容量比第i件物品体积大,且当前价值比更新后价值小,则更新

            if(j>=w[i]&&val[j-w[i]]+d[i]>val[j])

                val[j]=val[j-w[i]]+d[i];

        }

    return ;

}

 

int main()

{

 freopen("01beibao_一维.in","r",stdin);

    int i;

    while(scanf("%d%d",&n,&W)!=EOF)

    {

        for(i=1;i<=n;i++)

            scanf("%d%d",&w[i],&d[i]);

        knapsack();

        printf("%d\n",val[W]);

    }

    return 0;

}

//01beibao_一维.in内容如下:

//5 20

//17 92

//9 22

//4 80

//11 240

//19 90

//初始化的细节问题

//我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。

//如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。

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

//为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。

//这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。

//一个常数优化

//前面的伪代码中有 for v=V..1,可以将这个循环的下限进行改进。

//由于只需要最后f[v]的值,倒推前一个物品,其实只要知道f[v-w[n]]即可。以此类推,对以第j个背包,其实只需要知道到f[v-//sum{w[j..n]}]即

//可,即代码中的

//for i=1..N   // for v=V..0

//可以改成

//for i=1..n    //bound=max{V-sum{w[i..n]},c[i]}    //for v=V..bound

//这对于V比较大时是有用的。

//小结

//01背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转

//成01背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。

//

//

//--如果对 01背包的二维数组实现方式不清晰,可以参考 金矿模型



0 0