解0-1背包问题

题目:

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

  本文按照动态规划的标准模式解析:http://blog.csdn.net/hearthougan/article/details/53749841

 0-1背包问题,表示的是每个物品只有一件,每件物品不能分割,在不超过背包容量的同时,如何选取物品,使得背包所装的价值最大(背包可以装不满)。这是一个经典的动态规划问题,有《背包九讲》珠玉在前,我所能做的也只是按自己的理解,加以分析这个问题。如果需要《背包九讲》请点击:

http://download.csdn.net/detail/hearthougan/5891267

  不妨设集合表示n个物品,该问题的某一个最优解集合为

1、最优子结构

  我们已经假设该问题的最优解为A,那么对于某个物品,令,表示原问题除去物品后的一个子问题,那么,就是该子问题的一个最优解。可反证此问题,即存在一个是子问题的最优解解,那么,所得的集合一定比原问题最优解集合S所得的最大价值要大,这与假设矛盾。因此原问题的最优解一定包含子问题的最优解,这就证明了最优子结构性质。

2、递归地定义最优解的值

  对于每个物品我们可以有两个选择,放入背包,或者不放入,有n个物品,故而我们需要做出n个选择,于是我们设f[i][v]表示做出第i次选择后,所选物品放入一个容量为v的背包获得的最大价值。现在我们来找出递推公式,对于第i件物品,有两种选择,放或者不放。

  <1>:如果放入第i件物品,则f[i][v] = f[i-1][v-w[i]]+p[i],表示,前i-1次选择后所选物品放入容量为v-w[i]的背包所获得最大价值为f[i-1][v-w[i]],加上当前所选的第i个物品的价值p[i]即为f[i][v]。

  <2>:如果不放入第i件物品,则有f[i][v] = f[i-1][v],表示当不选第i件物品时,f[i][v]就转化为前i-1次选择后所选物品占容量为v时的最大价值f[i-1][v]。则:

f[i][v] = max{f[i-1][v], f[i-1][v-w[i]]+p[i]}

3、求解最优值

我们根据2的递推公式,可以实现代码如下:

 

[cpp] view plain copy print?

  1. #include <iostream>  
  2. #include <cstdio>  
  3. #include <cstdlib>  
  4.   
  5. using namespace std;  
  6.   
  7. const int MAXN = 100;  
  8.   
  9. int main()  
  10. {  
  11.     int n, V;  
  12.     int f[MAXN][MAXN];  
  13.     int w[MAXN], p[MAXN];  
  14.   
  15.     while(cin>>n>>V)  
  16.     {  
  17.         if(n == 0 && V == 0)  
  18.             break;  
  19.         for(int i = 0; i <= n; ++i)  
  20.         {  
  21.             w[i] = 0, p[i] = 0;  
  22.             for(int j = 0; j <= n; ++j)  
  23.             {  
  24.                 f[i][j] = 0;  
  25.             }  
  26.         }  
  27.         for(int i = 1; i <= n; ++i)  
  28.             cin>>w[i]>>p[i];  
  29.         for(int i = 1; i <= n; ++i)  
  30.         {  
  31.             for(int v = w[i]; v <= V; ++v)  
  32.             {  
  33.                 f[i][v] = max(f[i-1][v], f[i-1][v-w[i]]+p[i]);  
  34.             }  
  35.         }  
  36.         cout<<f[n][V]<<endl;  
  37.     }  
  38.     return 0;  
  39. }  
#include <iostream>
#include <cstdio>
#include <cstdlib>

using namespace std;

const int MAXN = 100;

int main()
{
    int n, V;
    int f[MAXN][MAXN];
    int w[MAXN], p[MAXN];

    while(cin>>n>>V)
    {
        if(n == 0 && V == 0)
            break;
        for(int i = 0; i <= n; ++i)
        {
            w[i] = 0, p[i] = 0;
            for(int j = 0; j <= n; ++j)
            {
                f[i][j] = 0;
            }
        }
        for(int i = 1; i <= n; ++i)
            cin>>w[i]>>p[i];
        for(int i = 1; i <= n; ++i)
        {
            for(int v = w[i]; v <= V; ++v)
            {
                f[i][v] = max(f[i-1][v], f[i-1][v-w[i]]+p[i]);
            }
        }
        cout<<f[n][V]<<endl;
    }
    return 0;
}

  由此我们可以知道,我们必须要做出n次选择,所以外层n次循环是必不可少的,对于上面代码的内层循环,表示当第i个商品要放入容量为v(v = w[i]....V)的背包时所获得的价值,即先对子问题求解,这个也是必不可少的,所以时间复杂度为O(nV),这个已不能进一步优化,但是我们可以对空间进行优化。

 

  由于我们用f[n][V]表示最大价值,但是当物品和背包容量比较大时,这种用法会占用大量的空间,那么我们是不是对此可以进一步优化呢?

  现在考虑如果我们只用一位数组f[v]来表示f[i][v],是不是同样可以达到效果?我们由上述可知f[i][v]是由f[i-1][v]和f[i-1][v-w[i]]这两个子问题推得,事实上第i次选择时,我们虽用到前i-1次的最优结果,但是前i-1次选择的最优结果,已经保存在做出第i-1次选择后的结果中,即第i次的结果只用到了第i-1次选择后的状态因此我们可以只用一维数组来维持每次选择的结果,怎么维持?也就是当第i次选择时,我们怎么得到f[i-1][v]和f[i-1][v-w[j]]这两种状态,即第i次求f[v]时,此时f[v]和f[v-w[i]]表示的是不是f[i-1][v]和f[i-1][v-w[j]]事实上我们只需要将内层循环变为从V到w[j]的逆向循环即可满足要求。这句话不是很好理解,我们先给出优化后的代码,然后由图表来慢慢分析。

 

[cpp] view plain copy print?

  1. #include <iostream>  
  2. #include <cstdio>  
  3. #include <cstdlib>  
  4. #include <cstring>  
  5. using namespace std;  
  6.   
  7. const int MAXN = 100;  
  8.   
  9. int main()  
  10. {  
  11.     int n, V;  
  12.     int f[MAXN];  
  13.     int w[MAXN], p[MAXN];  
  14.   
  15.     while(cin>>n>>V)  
  16.     {  
  17.         if(n == 0 && V == 0)  
  18.             break;  
  19.   
  20.         memset(f, 0, sizeof(f));  
  21.         memset(w, 0, sizeof(w));  
  22.         memset(p, 0, sizeof(p));  
  23.   
  24.         for(int i = 1; i <= n; ++i)  
  25.             cin>>w[i]>>p[i];  
  26.   
  27.         for(int i = 1; i <= n; ++i)  
  28.         {  
  29.             for(int v = V; v >= w[i]; --v)  
  30.             {  
  31.                 f[v] = max(f[v], f[v-w[i]]+p[i]);  
  32.             }  
  33.         }  
  34.         cout<<f[V]<<endl;  
  35.     }  
  36.     return 0;  
  37. }  
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;

const int MAXN = 100;

int main()
{
    int n, V;
    int f[MAXN];
    int w[MAXN], p[MAXN];

    while(cin>>n>>V)
    {
        if(n == 0 && V == 0)
            break;

        memset(f, 0, sizeof(f));
        memset(w, 0, sizeof(w));
        memset(p, 0, sizeof(p));

        for(int i = 1; i <= n; ++i)
            cin>>w[i]>>p[i];

        for(int i = 1; i <= n; ++i)
        {
            for(int v = V; v >= w[i]; --v)
            {
                f[v] = max(f[v], f[v-w[i]]+p[i]);
            }
        }
        cout<<f[V]<<endl;
    }
    return 0;
}

  例,有3个物品,背包容量为10,如下:

 

 

  初始时:我们初始化f[]全部为0

 

  第1次主循环,即当i = 1时,我们只对物品1进行选择,对于内层循环,即当v = 10....3时,我们有:

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

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

  f[8] =max{f[8], f[8-3]+p[1]} = max{f[8], f[5]+4} = max{0, 0+4} = 4;

  f[7] = max{f[7], f[7-3]+p[1]} = max{f[7], f[4]+4} = max{0, 0+4} = 4;

  f[6] = max{f[6], f[6-3]+p[1]} = max{f[6], f[3]+4} = max{0, 0+4} = 4;

  f[5] = max{f[5], f[5-3]+p[1]} = max{f[5], f[2]+4} = max{0, 0+4} = 4;

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

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

  f[2] = f[1] = f[0] = 0;

  其中f[2] = f[1] = f[0] = 0,是因为体积为3的物品,根本不会影响当背包容量为2、1、0时的状态。所以他们依旧保持原来的状态。对应于:

  表中横轴的蓝色区域,表示当容量为v时,对第1个商品做出选择时所依赖的上一层的状态,如当v=10时,所依赖的就是f[0][10]和f[0][7]两个状态。所以当计算f[1][v](v = 10....3)时,f[v]和[v-3]保存的就是f[0][v]和f[0][v-3]。

  第2次循环,即i = 2时,我们对物品2做出选择:

  f[10] = max{f[10], f[10-4]+p[2]} = max{f[10], f[6]+p[2]} = max{4, 4+5} = 9

  f[9] = max{f[9], f[9-4]+p[2]} = max{f[9], f[5]+p[2]} = max{4, 4+5} = 9

  f[8] = max{f[8], f[8-4]+p[2]} = max{f[8], f[4]+p[2]} = max{4, 4+5} = 9

  f[7] = max{f[7], f[7-4]+p[2]} = max{f[7], f[3]+p[2]} = max{4, 4+5} = 9

  f[6] = max{f[6], f[6-4]+p[2]} = max{f[6], f[2]+p[2]} = max{4, 0+5} = 5

  f[5] = max{f[5], f[5-4]+p[2]} = max{f[5], f[1]+p[2]} = max{4, 0+5} = 5

  f[4] = max{f[4], f[4-4]+p[2]} = max{f[4], f[0]+p[2]} = max{4, 0+5} = 5

  f[3] = 4

  f[2] = f[1] = f[0] = 0;

  第3次循环,即当i = 3时

  f[10] = max{f[10], f[10-5]+p[3]} = max{f[10], f[5]+3} = max{9, 5+6} = 11

  f[9] = max{f[9], f[9-5]+p[3]} = max{f[9], f[4]+3} = max{9, 5+6} = 11

  f[8] = max{f[8], f[8-5]+p[3]} = max{f[8], f[3]+3} = max{9, 4+6} = 10

  f[7] = max{f[7], f[7-5]+p[3]} = max{f[7], f[2]+3} = max{9, 0+6} = 9

  f[6] = max{f[6], f[6-5]+p[3]} = max{f[6], f[1]+3} = max{5, 0+6} = 6

  f[5] = max{f[5], f[5-5]+p[3]} = max{f[5], f[0]+3} = max{5, 0+6} = 6

  f[4] = 5

  f[3] = 4

  f[2] = f[1] = f[0]

  对于应表:

  综上的步骤我们可以很清楚的知道v从大到小循环可以满足题意,从表中我们也可以知道,我们如用f[i][v]存储最优结果,有很多没用的求解结果也被保存下来,从而浪费了大量的空间。如果我们用f[v],那么保存的就是最终结果,且很好地利用了空间。

  如果还是疑问为什么v从小到大的顺序不可以(即内循环为:for(int v = w[i]; v <= V; ++v)),我们依旧可以按着代码,试着画一下表,一次,就很清楚了,比如同样的问题,正着走一次为:

  当i=1时:

  f[0] = f[1] = f[2] = 0

  f[3] = max{f[3], f[3-3]+p[1]} = max{0, 0+4} = 4

  f[4] = max{f[4], f[4-3]+p[1]} = max{0, 0+4} = 4

  f[5] = max{f[5], f[5-3]+p[1]} = max{0, 0+4} = 4

  以上结果貌似是对的,但是意思已经完全不是我们当初设计代码时的思想了,注意下面:

  f[6] = max{f[6], f[6-3]+p[1]} = max{0. 4+4} = 8//此时我们用的f[6]和f[3]相当于是f[1][6]和f[1][3],而不是f[0][6]和f[0][3]!

  f[7] = max{f[7], f[7-3]+p[1]} = max{0, 4+4} = 8

  f[8] = max{f[8], f[8-3]+p[1]} = max{0, 4+4} = 8

  f[9] = max{f[9], f[9-3]+p[1]} = max{0, 8+4} = 12

  f[10] = max{f[10], f[10-3]+p[1]} = max{0, 8+4} = 12

  到此,我们清楚了为什么正向循环为何不可,因为此时f[v]保存的相当于是f[i][v]和f[i][v-w[i]],这已经违背了我们意图!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值