背包问题之多重背包

多重背包

不难发现多重背包完全背包的区别仅在于多重背包中每个物品的数量有限。
朴素做法:直接拿完全背包的代码,只加了一个判断条件k<=s[i]限制一下就算背包还能放下但物品数量有限的情况就行
#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int v[N],w[N],s[N];
int n,m;
int f[N][N];
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            for(int k=0;k*v[i]<=j&&k<=s[i];k++){
                f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }
    cout<<f[n][m];
    return 0;
}

那么能否使用类似完全背包的优化方式进行恒等变换呢? 答案是不能!

在这里插入图片描述

显然,按照完全背包的优化思路我们不难得出上图,但由于末尾多了一个式子导致我们无法进行完美的恒等变换,因为取最大值函数是非线性的,多一个式子少一个式子,得到的值可能天差地别,自然无法进行恒等变换了。
我们再浅析一下为什么会多出一项这个问题的本质:因为有限和无限其实是相对于背包的容量来讲的。
假设物品足够多背包容量有限,放i物品数目最终一定会达到背包的上限,假设此时放了k个,也就是说这个k其实是限死了,不论是f(i,j)还是f(i,j-v)最终都会到达f(i,j-k*v)这个背包的上限,而非物品的上限,背包的上限是一致的。
但是如果某个物品的数量小于背包容量,就会出现f(i,j-(k+1)*v)+k*w的情况。千万别看f(i,j)和f(i,j-v)貌似是后者多放了一个物品,所以物品总数应该比前面一个少1,最终推导出来可以等量代换,而对这个概念的模糊就是问题的关键。j-v只是表示此刻背包的体积罢了,与是否放物品并无关系,所以在背包足够大的情况下f(i,j-v)也能放下k个物品,也就是f(i,j-(k+1)*v)+k*w
最终结论是这种优化走不通,那我们再回顾一下朴素代码,外面两层循环差不多是背包问题的套路,少不了的,但最内层循环暴力枚举s[i]个物品可能拿几个这件事可以通过二进制来优化。

二进制优化

总所周知,一个字节8位,可以表示0~255这个范围。而255=2^0+2^1+2^2+...+2^7,其实这就是8位上每一位都取1的结果。也就是8位上取1的不同的组合能构成0~2^(k+1)-1范围内的所有数。这种组合问题恰好撞01背包上了。

那么我们能否将多重背包化成01背包来做呢?答案是可以的!

假设第i个物品s[i]=7,我们将其拆分、打包分成3个小物品,s[i1]=2^0,s[i2]=2^1,s[i3]=2^2。而这3个物品的任意选取组合刚好能覆盖0~7的所有取法。也就是说我不论在多重背包中的s[i]这里选取了多少个,都可以通过这3个拆分的子物品的选取,组合出你选的这个数据。如果我们将完全背包中的所有物品都这样拆分打包成小的子物品,然后对所有子物品做一遍01背包不就对了嘛~

时间复杂度从N*V*M降低到了N*logM*V,最后一层对s[i]的遍历直接没了,但是N个物品变成了N*logM个了,因为我们是按二进制拆分的。

如果第i个物品s[i]=200呢?还是按照一样的思路,先按2的次幂拆,能拆几个是几个,比如2^0+2^1+...+2^6=2^7-1=127也就是说,前面六个子物体全部选上,都只能至多凑到127,比200还要小呢。但是如果再打包一个出来2^0+2^1+...+2^6+2^7=2^8-1=255。也就是说我要是把打包出来的物品全选了,物品总数超过了200,而实际第i个物品只有200个,不合常理。最简单的办法就是将剩下不能构成2的次幂的物品单独打包成一个即200-127=73。最后一组只有73个i物品。

也就是说,我最终将200i物品拆分打包成了,1,2,4,8,16,32,64,738个子物品,每一个物品都是单独存在的,但是在01背包中,对他们的不同选择,即构成了0~200的所有可能的数字。

8个物品的所有组合,构成了0~200的所有可能,将本来200次的遍历,变成了最单纯的求组合问题,直接上01背包。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 12010, M = 2010;

int n, m;
int v[N], w[N];
int f[M];

int main()
{
    cin >> n >> m;

    int cnt = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1;
        while (k <= s)
        {
            cnt ++ ;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if (s > 0)
        {
            cnt ++ ;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

    n = cnt;

    for (int i = 1; i <= n; i ++ )
        for (int j = m; j >= v[i]; j -- )
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m] << endl;

    return 0;
}

做法和代码实现并不难,但为了尽量简单写明白这个过程我省去了很多证明,甚至在这个写的过程中频繁陷入牛角尖,本来了然于胸的证明在脑海中逐渐变得混沌。最后,不如无招胜有招,去他喵的证明,记住这个步骤,理解每一步的意思就足矣了,旁支末节的证明会就会不会就拉到,别给自己增加负担咯

所以上面都是废话,看下面这一句就行:

将多重背包转换成01背包来做,每个i物品按2的次幂,依次拆分打包成组,每组单独作为一个新物品,如果无法全部拆分为2的次幂,就把最后剩下的单独打包成一组。然后跑一边01背包就行。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值