详解0-1背包_动态规划

参考书目:算法与竞赛入门经典(第2版)刘汝佳

问题描述:

    有n个重量和价值分别为wi 和 vi 的 物品,从这些物品中选择总重量不超过 W 的物品,求所有挑选方案中物品价值总和的最大值。

分析:

0-1背包与完全背包不同之处在于,每件物品只能使用一次。而每装载一件物品,都可以得到解的一部分,也就是多阶段决策问题,回顾完全背包(硬币凑数问题),由于每件物品都可以无限使用,因此每一次可供选择的东西都是一样的。而在本问题中,每做出一次选择都会在两个方面(背包容量和可选择的东西)影响下一次的选择。如果仅仅按照完全背包那样定义状态(d[t]:表示在背包剩余容积t时,最优装载量)会导致我们无法确定某件物品是否被装载,为了解决这个问题,我们可以增加一个标志来表示当前的阶段,或者说物品,每次面对一个阶段(物品)做选择,然后进入下一个阶段,这样我们就可以保证,一个物品不会被重复选取,也就是有秩序了。我们就产生了一个天然的循环顺序:每一个阶段都是一个物品在等待被选择,在阶段里面就是考虑选于不选的问题了。

1.定义d[ i ][ j ] :表示在第 i 个阶段(面对第 i 个物品时),背包剩余体积 j 时,背包的最优装载量。

显然这时的状态转移方程为 :d[ i ][ j ] = max(d[ i +1][ j ],d[ i +1][ j - w[ i ] ] +v[ i ] )     (w[i] 和 v[i] : 第i个物品的重量和价值)这个状态转移方程解析:面对第 i 个物品可以选择装载和不装载:

装载的话:进入下一个阶段(考虑下一个物品):i+1,并且背包容量j减少对应物品的重量(体积):  j - w[i],然后价值增加对应物品的价值 v[i]。

不装载的话:放弃这个阶段(物品):i+1,背包容量不减少,价值不增加。

代码:

#include<bits/stdc++.h>

#define MAX 100
using namespace std;

int main()
{
    int n,s;
    int w[MAX];
    int v[MAX];
    int d[MAX][MAX];

    cin>>n>>s;

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

    for(int i = n ; i>=1 ; i--)//由于d[i][j]会利用到d[i+1][j-w[i]],所以必须让后者(i+1)先计算出来,因此需要逆向枚举,而j的枚举顺序则没有要求。
        for(int j = 0 ; j<=s ; j++)
        {
            d[i][j] = (i==n? 0:d[i+1][j]);//这里需要注意边界,所以事先处理好让d[i][j] = d[i+1][j](i!=n)
            if(j>=w[i])
                d[i][j] = max(d[i][j],d[i+1][j-w[i]]+v[i]); //这里max中第一个参数中i没有加1的原因已经说明,状态转移方程没有错
            }
              cout<<d[1][s];//由于逆向枚举,所以阶段1反而是最后一个阶段,s则是终点。
    return 0;
}

2.另一种对称的定义方法是:d[ i ][ j ]:把前i个物品装载到容量为 j 的背包中的最优装载量。

此时的状态转移方程为:d[i][j] = max( d[ i-1 ][ j ], d[ j-1][ j -w[i] ] + v[i] ),相信不少初学者第一眼都会觉得怎么会是这样,说好的对称呢?不应该是这样: d[i][j] = max( d[ i-1 ][ j ], d[ i-1][ j + w[i] ] - v[i] )?但是,这显然是错误的,背包的容量不可能增加。其实仔细思考一下就能理解了,我们可以用逆向思维来考虑:假设装好了前 i 件物品,那么要求第 d[i][j]就只用求第i-1件物品的d[i-1][j] 了,对于第i-1件物品它只需要满足背包容量为s-w[i]时,附加第i件物品的价值后求最大价值的要求以此类推。这个定义方法特点是在假设已经取得了前 i 个阶段的最优前提下,然后向前一个阶段求最优,直到到达最小问题,d[0][j] = 0.这种定义方法的好处在于可以按照正常顺序进行枚举物品,因为在 计算d[i][ ]前,d[i-1][ ] 已经计算出来了,这样一来,我们也没必要将物品全部存起来,可以边读边处理。

代码:

#include<bits/stdc++.h>

#define MAX 100
using namespace std;

int main()
{
    int n,s;
    int w;
    int v;
    int d[MAX][MAX];

    cin>>n>>s;

    for(int i = 1 ; i<=n ; i++)//注意这里的枚举顺序
    {
        cin>>w>>v;//临时读入
        for(int j = 0 ; j<=s ; j++)
        {
            d[i][j] = (i==1? 0:d[i-1][j]);
            if(j>=w)
                d[i][j] = max(d[i][j],d[i-1][j-w]+v);
        }
    }
    cout<<d[n][s];
    return 0;
}

2-1.优化版本:

如果我们将2的代码,中 j 的枚举顺序改成逆向枚举会发生什么呢? 此时会有:

假设现在d[i][j]还未计算,则d[i][j]会等于d[i-1][j] , 而d[i-1][j-v]的值一定会后于的d[i][j]的值算出(因为j是逆向枚举,j-v < j),而此时d[i][j]都还没计算出,所以d[i-1][j-v]必然没有计算出,还是初值也就是d[i-1][j-v]。因为二维数组d的计算方向是由上往下由右往左的(详细见图,图中1,2两条线表示该格数据的来源,显然,我们如果把第三行的数据覆盖到第2行或者第1行是可行的,因为不管是哪一种来源在使用之前它们的数据都不会被覆盖,而在使用之后才会被覆盖)由状态转移方程:d[i][j] = max(d[i-1][j],d[i-1][j-w]+v)可以看出,d[i][j]与后面的max中的两个值值相差一行。所以我们考虑只用一行来存储。

这样新的状态转移方程就是:f[j] = max(f[ j ],f[j-w]+v) 

代码:

#include<bits/stdc++.h>

#define MAX 100
using namespace std;

int main()
{
    int n,s;
    int w;
    int v;
    int f[MAX];

    cin>>n>>s;
    memset(f,0,sizeof(f));
    for(int i = 1 ; i<=n ; i++)
    {
        cin>>w>>v;
        for(int j = s ; j>=0 ; j--)
        {
            if(j>=w)
                f[j] = max(f[j],f[j-w]+v);//这里不用考虑边界了
        }
    }
    cout<<f[s];
    return 0;
}

总结:无论哪一种第一方法其实都是在不断的缩小问题的规模的过程,将处于每个阶段的最优都求出来,然后在下一个阶段选择时再取最优,就能保证一直最优。

猜你喜欢

转载自blog.csdn.net/SWEENEY_HE/article/details/82227640