背包9讲

背包9讲

http://cuitianyi.com/

https://github.com/tianyicui/pack

 

刘汝佳 9.3.2 01背包的两种递推公式都是基于对第i件物品的策略(放或不放)

第2种递推

f(i,j)表示“把前i个物品装到容量为j的背包中的最大总重量”,其状态转移方程也不难得出:

  f(i, j) = max{f(i-1,j), f(i-1, j-V[i]) + W[i]}                       

边界是类似的:i=0时为0,j<0时为负无穷,最终答案为f(n,C)。代码也是类似的:

for(int i = 1; i <= n; i++)

  for(int j = 0; j <= C; j++){

    f[i][j] = (i==1 ? 0 : f[i-1][j]);

    if(j >= V[i]) f[i][j] = max(f[i][j], f[i-1][j-V[i]]+W[i]);

    }

以上代码中 i==1的时候f[i][j] 初始化为0,相当于事先在双层for循环之前初始化所有f[0][j] = 0,然后再这里直接赋值 f[i][j] = f[i-1][j]

使用滚动数组时,j需要逆序遍历,原因如下,参考 http://blog.csdn.net/insistgogo/article/details/8579597

优化空间复杂度

上述的方法,我们使用二维数组 f[i][v] 保存中间状态,这里我们可以使用一维数组f[v]保存中间状态就能得到结果

 

分析

我们现在使用f[v]保存中间状态,我们想要达到的效果是,第i次循环后,f[v]中存储的是前i个物体放到容量v时的最大价值

在回顾下之前讲过的状态转移方程:

f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + cost[i])

我们可以看到,要想得到 f[i][v],我们需要知道 f[i - 1][v] 和 f[i - 1][v - weight[i]],由于我们使用二维数组保存中间状态,所以可以直接取出这两个状态。

当我们使用一维数组存储状态时,f[v]表示,在执行i次循环后(此时已经处理i个物品),前i个物体放到容量v时的最大价值,即之前的f[i][v]。与二维相比较,它把第一维隐去了,但是二者表达的含义还是相同的,只不过针对不同的i,f[v]一直在重复使用,所以,也会出现第i次循环可能会覆盖第i - 1次循环的结果。

为了求f[v],我们需要知道,前i - 1个物品放到容量v的背包中带来的收益,即之前的f[i - 1][v]  和 前i - 1件物品放到容量为v - weight[i]的背包中带来的收益,即之前的f[i - 1][v - weight[i]] + cost[i]。

 

难点

由于我们只使用一维数组存储,则在求这两个子问题时就没有直接取出那么方便了,因为,第i次循环可能会覆盖第i - 1次循环的结果。

现在我们来求这两个值

1)前i - 1个物品放到容量v的背包中带来的收益,即之前的f[i - 1][v] :

由于,在执行在i次循环时,f[v]存储的是前i个物体放到容量v时的最大价值,在求前i个物体放到容量v时的最大价值(即之前的f[i][v])时,我们是正在执行第 i 次循环,f[ v ]的值还是在第 i - 1  次循环时存下的值,在此时取出的 f[ v ]就是前i - 1个物体放到容量v时的最大价值,即f[i - 1][v]。

2)前i - 1件物品放到容量为v - weight[i]的背包中带来的收益,即之前的f[i - 1][v - weight[i]] + cost[i]

由于,在执行第i次循环前,f[0 ~ V]中保存的是第i - 1次循环的结果,即是前i - 1个物体分别放到容量0 ~ V时的最大价值,即f[i - 1][0 ~ V]。

则,在执行第i次循环前,f 数组中v - weight[i]的位置存储就是我们要找的 前i - 1件物品放到容量为v - weight[i]的背包中带来的收益 (即之前的f[i - 1][v - weight[i]]),这里假设物品是从数组下标1开始存储的。

 

伪代码

for i=1..N //枚举物品
    for v=V..0 //枚举容量,从大到小
        f[v]=max{f[v],f[v-weight[i]] + cost[i]};

由上面伪代码可知,在执行第 i 次循环时,需要把背包容量由V..0都要遍历一遍,检测第 i 件物品是否能放。

 

逆序枚举容量的原因

注意一点,我们是由第 i - 1 次循环的两个状态推出 第 i 个状态的,而且 v  > v - weight[i],则对于第i次循环,背包容量只有当V..0循环时,才会先处理背包容量为v的状况,后处理背包容量为 v-weight[i] 的情况。

具体来说,由于,在执行v时,还没执行到v - weight[i]的,因此,f[v - weight[i]]保存的还是第i - 1次循环的结果。即在执行第i次循环 且 背包容量为v时,此时的f[v]存储的是 f[i - 1][v] ,此时f[v-weight[i]]存储的是f[i - 1][v-weight[i]]。

相反,如果在执行第 i 次循环时,背包容量按照0..V的顺序遍历一遍,来检测第 i 件物品是否能放。此时在执行第i次循环 且 背包容量为v时,此时的f[v]存储的是 f[i - 1][v] ,但是,此时f[v-weight[i]]存储的是f[i][v-weight[i]]。

因为,v  > v - weight[i],第i次循环中,执行背包容量为v时,容量为v - weight[i]的背包已经计算过,即f[v - weight[i]]中存储的是f[i][v - weight[i]]。即,对于01背包,按照增序枚举背包容量是不对的。

 

另外,可以用图形形象化的说明逆序遍历容量的原因

不管正序还是逆序,v在一次for循环中设置了f[0...V]中的所有元素,可以看做从左到右(正序)或从右到左(逆序)对数组f[0...V]刷了颜色,每次for循环使用不同的颜色

如果是正序,因为v - weight[i] < v,在设置f[v]的时候已经设置过f[v - weight[i]],f[v - weight[i]]的颜色已经更新了,代表f[i][v - weight[i]],错误

如果是逆序,在设置f[v]的时候,f[v - weight[i]]的颜色还没更新,代表f[i-1][v - weight[i]],正确

 

http://poj.org/problem?id=3624 是01背包问题,需要滚动数组,以减少内存消耗

 

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>

using namespace std;

int main()
{
    int N, M; //1 <= N <= 3402, 1 <= M <= 12880
    //int W[12900];
    //int D[12900];
    int W, D;
    int f[12900];
    while (scanf("%d%d", &N, &M) != EOF ){ // 如果这里的 != EOF 去掉,提交的时候出现 Output Limit Exceeded
        /*
        for (int i = 1; i <= N; i++){
            scanf("%d%d", &W[i], &D[i]);
        }
        */

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

        for (int i = 1; i <= N; i++) {
            scanf("%d%d", &W, &D);
            for (int j = M; j >= 0; j--){
                if (j >= W)
                    f[j] = max(f[j], f[j - W] + D);
            }
        }

        printf("%d\n", f[M]);
    }

    return 0;
}

 

例题9-5 劲歌金曲 UVa 12563

参考 http://blog.csdn.net/u013480600/article/details/40376143

这里对01背包里公式里面的数组f[]的类型,max函数,和状态切换进行抽象,

在传统的01背包中,数组f[]的类型是 int,代表容量这一个单一值,max函数用来比较容量的大小

转移方程是 f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + cost[i])

如果用滚动数组  f[v] = max(f[v],f[v - weight[i]] + cost[i])

f[v - weight[i]] + cost[i] 代表一个新的状态,max()函数用于在两个状态中选择一个最优的

 

劲歌金曲问题里面,数组f[]的类型包含两个int,分别代表歌曲的长度和歌曲的数量,自定义opt函数来比较两个状态

#define _CRT_SECURE_NO_WARNINGS 

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>

using namespace std;

// 参考 http://blog.csdn.net/u013480600/article/details/40376143

struct songState
{
    int len;
    int num;

    songState() : len(0), num(0){}
    songState(int l, int n) :len(l), num(n) {}
};

songState opt(songState s1, songState s2)
{
    if ((s1.num > s2.num) || ((s1.num == s2.num) && (s1.len > s2.len)))
        return s1;
    else
        return s2;
}

int main()
{
    int T; // T <= 100
    int n, max_t; // 1 <= n <= 50, 1 <= max_t <= 1000000000
    int t[55];
    songState f[180*50 + 5];

    int kase = 1;
    scanf("%d", &T);
    while (T--) {
        
        scanf("%d%d", &n, &max_t);
        max_t -= 1;

        int sum = 0;
        for (int i = 1; i <= n; i++) {
            scanf("%d", &t[i]);
            sum += t[i];
        }

        if (sum < max_t)
            max_t = sum;

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

        for (int i = 1; i <= n; i++){
            for (int j = max_t; j >= 0; j--){
                if (j >= t[i]){
                    f[j] = opt(f[j], songState(f[j - t[i]].len + t[i], f[j - t[i]].num + 1));
                }
            }
        }

        printf("Case %d: %d %d\n", kase++, f[max_t].num + 1, f[max_t].len + 678);
    }

    return 0;
}

 

posted @ 2016-08-18 13:58  PatrickZhou  阅读(371)  评论(0编辑  收藏  举报