0-1背包问题、贪心算法、动态规划

1、0-1背包问题

        0-1背包问题:有一个贼在偷窃一家商店时,发现有n件物品,第i件物品价值vi元,重wi磅,此处vi与wi都是整数。他希望带走的东西越值钱越好,但他的背包中至多只能装下W磅的东西,W为一整数。应该带走哪几样东西?这个问题之所以称为0-1背包,是因为每件物品或被带走;或被留下;小偷不能只带走某个物品的一部分或带走同一物品两次。

注:在选择装入背包的物品时,对每种物品i只有2种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i。(比如玉器,花瓶等)

       在(分数(部分))背包问题(fractional knapsack problem)中,场景与上面问题一样,但是窃贼可以带走物品的一部分,而不必做出0-1的二分选择。可以把0-1背包问题的一件物品想象成一个金锭,而部分问题中的一件物品则更像金沙。


2、贪心算法(按单位重量价值排序)(含为什么不可以解决)

      首先声明:虽然两个问题相似,但我们可以用贪心策略可以求解背包问题,而不能求解0-1背包问题,为了求解部分数背包问题,我们首先计算每个商品的每磅价值vi/wi。遵循贪心策略,小偷首先尽量多地拿走每磅价值最高的商品,如果该商品已全部拿走而背包未装满,他继续尽量多地拿走每磅价值第二高的商品,依次类推,直到达到重量上限W。因此,通过将商品按每磅价值排序,贪心算法的时间运行时间是O(nlgn)。


       对于这个问题,一开始确实有点不太好入手。一堆的物品,每一个都有一定的质量和价值,我们能够装入的总重量有限制,该怎么来装使得价值最大呢?对于这n个物品,每个物品我们可能会选,也可能不选,那么我们总共就可能有2^n种组合选择方式。如果我们采用这种办法来硬算的话,则整体的时间复杂度就达到指数级别的,肯定不可行。

         现在我们换一种思路。既然每一种物品都有价格和重量,我们优先挑选那些单位价格最高的是否可行呢?比如在下图中,我们有3种物品,他们的重量和价格分别是10, 20, 30 kg和60, 100, 120。

   《0-1背包问题、贪心算法、动态规划》《0-1背包问题、贪心算法、动态规划》

        那么按照单位价格来算的话,我们最先应该挑选的是价格为60的元素,选择它之后,背包还剩下50 – 10 = 40kg。再继续前面的选择,我们应该挑选价格为100的元素,这样背包里的总价值为60 + 100 = 160。所占用的重量为30, 剩下20kg。因为后面需要挑选的物品为30kg已经超出背包的容量了。我们按照这种思路能选择到的最多就是前面两个物品。如下图:

《0-1背包问题、贪心算法、动态规划》

        按照我们前面的期望,这样选择得到的价值应该是最大的。可是由于有一个背包重量的限制,这里只用了30kg,还有剩下20kg浪费了。这会是最优的选择吗?我们看看所有的选择情况:

《0-1背包问题、贪心算法、动态规划》

        很遗憾,在这几种选择情况中,我们前面的选择反而是带来价值最低的。而选择重量分别为20kg和30kg的物品带来了最大的价值。看来,我们刚才这种选择最佳单位价格的方式也行不通。



        网上基本上证明“贪心算法不能解决0-1背包问题“都是采用上面的例子,但是我起初在想可不可以在”按照单位重量价值“排序以后,从每个点都开始一次贪心遍历呢?比如上例,从10 走一趟,再从20走一趟。。。。。。

        天真了!主要是被上面例子迷惑了。。。。。

        看这个例子:背包可承受100

               id    ……     5      6       7       8        9…….       20

             v/w              5      6       7       8        9             1000000

              W               20   20      20     20      20             80

             

         在上面例子中,按贪心计算的话是选择5,6,7,8,9,,但是明显可以看出应该选5,20。也就是贪心算法无法从“当前全局”去决策!比如从5号开始,遍历到9号,已经背包满了,再往后遍历,即使有最优解也无法剔除前面已经装进去的。。。



最重要的原因是下面这条:

       对于0-1背包问题,贪心选择之所以不能得到最优解是因为:它无法保证最终能将背包装满,部分闲置的背包空间使每公斤背包空间的价值降低了

       事实上,在考虑0-1背包问题时,应比较选择该物品和不选择该物品所导致的最终方案,然后再作出最好选择。由此就导出许多互相重叠的子问题。这正是该问题可用动态规划算法求解的另一重要特征。


3、动态规划

       f[i][v]:表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。


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

       解释一下上面的方程:“将前i件物品放入容量为v的背包中”这个子问题,如果只考虑第i件物品放或者不放,那么就可以转化为只涉及前i-1件物品的问题:

        即1、如果不放第i件物品,则问题转化为“前i-1件物品放入容量为v的背包中”;

        2、如果放第i件物品,则问题转化为“前i-1件物品放入剩下的容量为v-weight[i]的背包中”,此时能获得的最大价值就是f [i-1][v-weight[i]]再加上通过放入第i件物品获得的价值value[i]。

        则f[i][v]的值就是1、2中最大的那个值。

[cpp] 
view plain
 copy

  1. // 背包问题    
  2. #include <iostream>     
  3. #include <algorithm>  
  4. using namespace std;  
  5.   
  6. #define N 3 // N件宝贝    
  7. #define C 5 // C是背包的总capacity    
  8.   
  9. int main()  
  10. {  
  11.     int value[N + 1] = { 0, 60, 100, 120 }; // 价值    
  12.     int weight[N + 1] = { 0, 1, 2, 3 };     // 重量    
  13.     int f[N + 1][C + 1] = { 0 };   // f[i][j]表示在背包容量为j的情况下,前i件宝贝的最大价值    
  14.   
  15.     int i = 1;  
  16.     int j = 1;  
  17.     for (i = 1; i <= N; i++)        //外循环控制物品数量,确保每个物品都会被遍历到  
  18.     {  
  19.         /*for (j = weight[i]; j <= C; j++)      //内循环控制物品的重量,确保能够遍历出“以前每个物品放入时的最大价值f[i][j]” 
  20.         { 
  21.             int x = f[i – 1][j];        //不放第i件物品 
  22.             int y = f[i – 1][j – weight[i]] + value[i];      //放入第i件物品 
  23.             f[i][j] = max(x, y); 
  24.         }*/  
  25.   
  26.         for (j = 1; j <= C; j++)  
  27.         {  
  28.             // 递推关系式    
  29.             if (j < weight[i])  
  30.             {  
  31.                 f[i][j] = f[i – 1][j];  
  32.             }  
  33.             else  
  34.             {  
  35.                 int x = f[i – 1][j];  
  36.                 int y = f[i – 1][j – weight[i]] + value[i];  
  37.                 f[i][j] = max(x, y);  
  38.             }  
  39.         }  
  40.     }  
  41.   
  42.     for (i = 0; i <= N; i++)  
  43.     {  
  44.         for (j = 0; j <= C; j++)  
  45.         {  
  46.             printf(“%4d “, f[i][j]);  
  47.         }  
  48.   
  49.         cout << endl;  
  50.     }  
  51.   
  52.     cout << endl << “选取的最大价值是:” << f[N][C] << endl;  
  53.     cout << “选取的物品如下:” << endl;  
  54.     i = N, j = C;  
  55.     while (i)  
  56.     {  
  57.         if (f[i][j] == (f[i – 1][j – weight[i]] + value[i]))  
  58.         {  
  59.             cout << i << “:” << “weight=” << weight[i] << “, value=” << value[i] << endl;  
  60.             j -= weight[i];  
  61.         }  
  62.         i–;  
  63.     }  
  64.   
  65.     cout << endl;  
  66.     return 0;  
  67. }  

运行结果:

《0-1背包问题、贪心算法、动态规划》

如果把上面代码中内循环改成注释部分,则运行结果如下:(个人认为写成注释部分的代码,更容易理解)

《0-1背包问题、贪心算法、动态规划》

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

        结合上面的例子,有三件物品,背包的最大负重量是5,求可以取得的最大价值。为了方面说明,物品weight依次为:1,2,3。二维数组下的求解顺序,物品数1—>n, 背包容量1—>w。如图,要使用一维数组,背包容量要采用倒序,即w—>1, 只有这样对于方程 dp( j ) = Max( dp( j ), dp (j-w[i] ) + v[i] ),才能达到等式左边才表示i,而等式右边表示i-1的效果。

《0-1背包问题、贪心算法、动态规划》


优化空间复杂度:

       上面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,物品大小weight{3,4,5},物品价值value{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]都是前一次循环产生的值。


f[0..10]初始值都为0。所以:

        当i=1时:

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

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

……

f[3]=max{f[3],f[3-weight[1]]+value[1]}=max{0,f[3]+4}=max{0,0+4}=4;

f[2]=max{f[2],f[2-weight[1]]+value[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-weight[i]]+value[i]}这个公式计算i=2时的f[0..10]的值。

        具体的值如下表所示:

《0-1背包问题、贪心算法、动态规划》

        因此,利用逆序循环就可以保证在计算f[v]时,公式  f[v]=max{f[v],f[v-weight[i]]+value[i]}  中 

               等号右边的  f[v]  和  f[v-weight[i]]+value[i]   保存的是    f[i-1][v]  和  f[i -1][v-weight[i]]  的值

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


[cpp] 
view plain
 copy

  1. #include <iostream>  
  2. #include <vector>  
  3. using namespace std;  
  4.   
  5. const int MIN = 0x80000000;  
  6. const int N = 3;   //物品数量  
  7. const int V = 5;  //背包容量  
  8. int f[V + 1];              // 一维数组  
  9.   
  10. int Package(int *W, int *C, int N, int V)  
  11. {  
  12.     int i, j;  
  13.     memset(f, 0, sizeof(f));  //初始化为0  
  14.   
  15.     for (i = 1; i <= V; i++)       //此步骤是解决是否恰好满足背包容量,  
  16.         f[i] = MIN;                // 若“恰好”满足背包容量,即正好装满背包,则加上此步骤; 若不需要“恰好”,则初始化为0  
  17.   
  18.     for (i = 1; i <= N; i++)  
  19.         for (j = V; j >= C[i]; j–)    //注意此处与解法一是顺序不同的,弄清原因  
  20.         {  
  21.             f[j] = (f[j]>f[j – C[i]] + W[i]) ? f[j] : (f[j – C[i]] + W[i]);  
  22.             cout << “f[“ << i << “][“ << j << “]=” << f[j] << endl;  
  23.         }  
  24.   
  25.     return f[V];  
  26. }  
  27.   
  28. void main()  
  29. {  
  30.     int W[4] = { 0, 7, 5, 8 };      //物品权重  
  31.     int C[4] = { 0, 2, 3, 4 };      //物品大小  
  32.   
  33.     int result = Package(W, C, N, V);  
  34.   
  35.     if (result > 0)  
  36.     {  
  37.         cout << endl;  
  38.         cout << “the opt value:” << result << endl;  
  39.     }  
  40.     else  
  41.         cout << “can not find the opt value” << endl;    // 可能不存在正好装满背包的解  
  42. }  

 在求最优解的背包问题中,一般有两种不同的问法:

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

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

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

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

很多内容来自:

http://www.cnblogs.com/fly1988happy/archive/2011/12/13/2285377.html

http://blog.csdn.net/sj13051180/article/details/6687674


    原文作者:贪心算法
    原文地址: https://blog.csdn.net/zwpf1994/article/details/79083972
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞