01背包问题,完全背包

转载:https://blog.csdn.net/hearthougan/article/details/53869671

题目:

  有 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的递推公式,可以实现代码如下:

#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]的逆向循环即可满足要求。这句话不是很好理解,我们先给出优化后的代码,然后由图表来慢慢分析。

#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]],这已经违背了我们意图!

完全背包:

完全背包问题与01背包问题的区别在于每一件物品的数量都有无限个,而01背包每件物品数量只有一个。

 解法:

        for i=1..N

  for j=0..V

  f[j]=max{f[j],f[j-c]+w}

  你会发现,这个伪代码01背包的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?

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

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

  这个算法也可以以另外的思路得出。例如,基本思路中的状态转移方程可以等价地变形成这种形式:

  f[j]=max{f[j],f[j-c]+w}

  将这个方程用一维数组实现,便得到了上面的伪代码。

  最后抽象出处理一件完全背包类物品的过程伪代码,以后会用到:

  procedure CompletePack(c,w)

  for j=c..V

  f[j]=max{f[j],f[j-c]+w}

题目:

有N种物品和一个容量为V的背包,每种物品都有无限件可用。

  第i种物品的体积是c,价值是w。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。这里不同之处是每件物品可无限取,这里就产生了很多可行的优化,比如同体积的有多种物品,则必然可以舍弃价值小的,也可以舍弃体积大于v的。

 基于以上理解在来解决这道题。

直接说题意,完全背包定义有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是c,价值是w。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。本题要求是背包恰好装满背包时,求出最大价值总和是多少。如果不能恰好装满背包,输出NO

输入

第一行: N 表示有多少组测试数据(N<7)。 
接下来每组测试数据的第一行有两个整数M,V。 M表示物品种类的数目,V表示背包的总容量。(0<M<=2000,0<V<=50000)
接下来的M行每行有两个整数c,w分别表示每种物品的重量和价值(0<c<100000,0<w<100000)

输出

对应每组测试数据输出结果(如果能恰好装满背包,输出装满背包时背包内物品的最大价值总和。 如果不能恰好装满背包,输出NO)

样例输入

2
1 5
2 2
2 5
2 2
5 1

样例输出                                      

              NO

             1

这里唯一不同的是背包如果不能完全装满,则输出NO,这里需要一个技巧,就是初始化时f[0],其余的均为-max,只有这样最大值为正时,只能通过f[0]在相加其他价值得到,如

背包体积为4时, 一种物品体积2,价值2;

  则   f[0]=0; f[1]=-max; f[2]=max(f[2],f[0]+w[i])=2; 注意若背包不需要全部装满时,f[3]本该为2的,但此时f[3]=max(f[3],f[1]+2)=max(f[3],2-max)=2-max; 负无穷

#include<bits/stdc++.h>
using namespace std;
int f[50000], c[2010], w[2010];
int main()
{
    int test, m, v, i, j;
    scanf("%d", &test);
    while (test--)
    {
        memset(f,-10,sizeof(f));//用来判断背包是否装满
        f[0] = 0;
        scanf("%d%d", &m, &v);
        for (i = 1; i <= m; ++i)
            scanf("%d%d", &c[i], &w[i]);
        for (i = 1; i <= m; ++i)
            for (j = 0; j <= v; ++j)//注意此循环与01背包的用一维数组表示的状态方程的区别,一个循环逆序,一个顺序
                if (j >= c[i])
                    f[j] = max( f[j] , f[j - c[i]] + w[i]);//完全背包的状态方程,可画图加深理解
                if (f[v]<0)//背包为装满
                    printf("NO\n");
        else
            printf("%d\n", f[v]);
    }
}

猜你喜欢

转载自blog.csdn.net/QLU_minoz/article/details/81209134