背包问题

来源:互联网 发布:淘宝介入买家都能赢吗 编辑:程序博客网 时间:2024/06/07 14:30

背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和无限背包,这里主要讨论01背包,即每个物品最多放一个。而无限背包可以转化为01背包。

先说一下算法的主要思想,利用动态规划来解决。每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中。即对于给定的n个物品,设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。再令v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值。则我们有下面的结果:

(1),v[i][0]=v[0][j]=0;

(2),v[i][j]=v[i-1][j]   当w[i]>j

(3),v[i][j]=max{v[i-1][j],v[i-1][j-w[i]]+v[i]}  当j>=w[i]

好的,我们的算法就是基于此三个结论式。

一、01背包:

1、二维数组法

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class sf {  
  2.   
  3.     public static void main(String[] args) {  
  4.         // TODO Auto-generated method stub  
  5.         int[] weight = {3,5,2,6,4}; //物品重量  
  6.         int[] val = {4,4,3,5,3}; //物品价值  
  7.         int m = 12//背包容量  
  8.         int n = val.length; //物品个数  
  9.           
  10.         int[][] f = new int[n+1][m+1]; //f[i][j]表示前i个物品能装入容量为j的背包中的最大价值  
  11.         int[][] path = new int[n+1][m+1];  
  12.         //初始化第一列和第一行  
  13.         for(int i=0;i<f.length;i++){  
  14.             f[i][0] = 0;  
  15.         }  
  16.         for(int i=0;i<f[0].length;i++){  
  17.             f[0][i] = 0;  
  18.         }  
  19.         //通过公式迭代计算  
  20.         for(int i=1;i<f.length;i++){  
  21.             for(int j=1;j<f[0].length;j++){  
  22.                 if(weight[i-1]>j)  
  23.                     f[i][j] = f[i-1][j];  
  24.                 else{  
  25.                     if(f[i-1][j]<f[i-1][j-weight[i-1]]+val[i-1]){  
  26.                         f[i][j] = f[i-1][j-weight[i-1]]+val[i-1];  
  27.                         path[i][j] = 1;  
  28.                     }else{  
  29.                         f[i][j] = f[i-1][j];  
  30.                     }  
  31.                     //f[i][j] = Math.max(f[i-1][j], f[i-1][j-weight[i-1]]+val[i-1]);  
  32.                 }  
  33.             }  
  34.         }  
  35.         for(int i=0;i<f.length;i++){  
  36.             for(int j=0;j<f[0].length;j++){  
  37.                 System.out.print(f[i][j]+" ");  
  38.             }  
  39.             System.out.println();  
  40.         }  
  41.           
  42.         int i=f.length-1;  
  43.         int j=f[0].length-1;  
  44.         while(i>0&&j>0){  
  45.             if(path[i][j] == 1){  
  46.                 System.out.print("第"+i+"个物品装入 ");  
  47.                 j -= weight[i-1];  
  48.             }  
  49.             i--;  
  50.         }  
  51.           
  52.     }  
  53.   
  54. }  

输出:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. 0 0 0 0 0 0 0 0 0 0 0 0 0   
  2. 0 0 0 4 4 4 4 4 4 4 4 4 4   
  3. 0 0 0 4 4 4 4 4 8 8 8 8 8   
  4. 0 0 3 4 4 7 7 7 8 8 11 11 11   
  5. 0 0 3 4 4 7 7 7 8 9 11 12 12   
  6. 0 0 3 4 4 7 7 7 8 10 11 12 12   
  7. 4个物品装入 第3个物品装入 第1个物品装入   

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

先考虑上面讲的基本思路如何实现,肯定是有一个主循环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]]的值。伪代码如下:

for i=1..N

    for v=V..0

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

其中的f[v]=max{f[v],f[v-c[i]]}一句恰就相当于我们的转移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},因为现在的f[v-c[i]]就相当于原来的f[i-1][v-c[i]]。如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]f[i][v-c[i]]推知,与本题意不符,但它却是另一个重要的背包问题P02最简捷的解决方案,故学习只用一维数组解01背包问题是十分必要的。

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

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

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

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


2、一维数组法(无须装满)

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class sf {  
  2.   
  3.     public static void main(String[] args) {  
  4.         // TODO Auto-generated method stub  
  5.         int[] weight = {3,5,2,6,4}; //物品重量  
  6.         int[] val = {4,4,3,5,3}; //物品价值  
  7.         int m = 12//背包容量  
  8.         int n = val.length; //物品个数  
  9.           
  10.         int[] f = new int[m+1];  
  11.         for(int i=0;i<f.length;i++){     //不必装满则初始化为0  
  12.             f[i] = 0;  
  13.         }  
  14.         for(int i=0;i<n;i++){  
  15.             for(int j=f.length-1;j>=weight[i];j--){  
  16.                 f[j] = Math.max(f[j], f[j-weight[i]]+val[i]);  
  17.             }  
  18.         }  
  19.         for(int i=0;i<f.length;i++){  
  20.             System.out.print(f[i]+" ");  
  21.         }  
  22.         System.out.println();  
  23.         System.out.println("最大价值为"+f[f.length-1]);  
  24.     }  
  25. }  

输出 

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. 0 0 3 4 4 7 7 7 8 9 11   
  2. 最大价值为11  

3、一维数组法(必须装满)

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class sf {  
  2.   
  3.     public static void main(String[] args) {  
  4.         // TODO Auto-generated method stub  
  5.         int[] weight = {3,5,2,6,4}; //物品重量  
  6.         int[] val = {4,4,3,5,3}; //物品价值  
  7.         int m = 12//背包容量  
  8.         int n = val.length; //物品个数  
  9.           
  10.         int[] f = new int[m+1];  
  11.         for(int i=1;i<f.length;i++){     //必装满则f[0]=0,f[1...m]都初始化为无穷小  
  12.             f[i] = Integer.MIN_VALUE;  
  13.         }  
  14.         for(int i=0;i<n;i++){  
  15.             for(int j=f.length-1;j>=weight[i];j--){  
  16.                 f[j] = Math.max(f[j], f[j-weight[i]]+val[i]);  
  17.             }  
  18.         }  
  19.         for(int i=0;i<f.length;i++){  
  20.             System.out.print(f[i]+" ");  
  21.         }  
  22.         System.out.println();  
  23.         System.out.println("最大价值为"+f[f.length-1]);  
  24.     }  
  25.   
  26. }  

输出

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. 0 -2147483648 3 4 3 7 6 7 8 10 11 12 11   
  2. 最大价值为11  

二、完全背包

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

但我们有更优的O(VN)的算法。

O(VN)的算法

这个算法使用一维数组,先看伪代码:

for i=1..N

    for v=0..V

        f[v]=max{f[v],f[v-cost]+weight}

你会发现,这个伪代码与P01的伪代码只有v的循环次序不同而已。

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class test{  
  2.       public static void main(String[] args){  
  3.            int[] weight = {3,4,6,2,5};  
  4.            int[] val = {6,8,7,5,9};  
  5.            int maxw = 10;  
  6.            int[] f = new int[maxw+1];  
  7.            for(int i=0;i<f.length;i++){  
  8.                f[i] = 0;  
  9.            }  
  10.            for(int i=0;i<val.length;i++){  
  11.                for(int j=weight[i];j<f.length;j++){  
  12.                    f[j] = Math.max(f[j], f[j-weight[i]]+val[i]);  
  13.                }  
  14.            }  
  15.            System.out.println(f[maxw]);  
  16.       }  
  17.     }  
输出

25

0 0