多重背包问题

3 篇文章 0 订阅

前言

没有理解01背包之前不推荐先玩别的背包,01背包的概念是一定要弄清楚的,完全背包的 顺推 和01背包的 逆推 思路也要理解清楚。
可参考以我发表的两篇博文:
01背包
完全背包

问题

题目链接:多重背包
大概就是这样了,输入的时候注意字母的区分,不要和核心代码部分混在一起。

字母含义表:

n n n m m m
希望购买的奖品的种数拨款金额

续表:

v i v_i vi w i w_i wi s i s_i si
价格价值能购买的最大数量(买 0 0 0件到 s s s件均可)

数据范围

v ≤ 100 , w ≤ 1000 , s ≤ 10 v\le 100,w\le 1000,s\le 10 v100,w1000,s10


朴素 · 思路

输入部分注意区分字母,在此就不多作赘述了。

好了,这里开始讲思路。
最容易想到的算法是把每件物品的数量拆成循环然后当成01背包做。
但是这个算法和多重背包差不多,都要注意几点:

  1. 在状态转移方程内,要把物品的重量和价值扩大 k k k 倍(不明确指出时, k k k 默认为物品件数的循环变量)。
  2. 注意下限为 0 0 0
  3. 注意上线的判断终止条件。

上限

上限现在具有两个判断条件:

  1. k × w i ≤ j k\times w_i\le j k×wij
  2. k ≤ s i k\le s_i ksi

此处 s i s_i si 为该物品的数量。

其实还可以这么写:
k ≤ m i n ( j ÷ w i , s [ i ] ) k\le min(j\div w_i,s[i]) kmin(j÷wi,s[i])
PS(整型变量相除默认向下舍入)
这么写可以节省亿——点点时间,创建一个变量维护这个值就可以了。

循环结构

大概长这样:

// 外面的东西
int x/*这里你随便搞个什么都行*/ = min(j / v[i], s[i])
for (int k = 0; k <= x; ++k) { // 或 k * v[i] <= j && k <= s[i]
	dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]);
}
// ∞ 个 '}'
// ┬┴┬┴┤・ω・)ノ
// 输出答案

朴素 · 代码

#include <iostream>
using namespace std;

int main()
{
    int n, m;
    cin >> n >> m;
    const int N = n + 1, M = m + 1;
    int v[N], w[N], s[N], dp[M];
    // price value  number
    for (int i = 1; i < N; ++i) cin >> v[i] >> w[i] >> s[i];
    for (int i = 0; i < M; ++i) dp[i] = 0;
    for (int i = 1; i < N; ++i) {
        for (int j = m; j >= 0; --j) {
            int x = min(s[i], j / v[i]);
            for (int k = 0; k <= x; ++k)
                dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]);
        }
    }
    cout << dp[m] << endl;
}

小贴士

小贴士:其实你完全没有必要按照题目的格式定义数组名,你完全可以把 a a a改成 b b b(仅举一例,请参照题目里的名字)

名称改变参考表:

v v v w w w s s s
w w w c c c v v v(其实这个可以不用改)

优化 · 思路

本节我们将深入讨论多重背包的二进制优化。(这里 “二” 的一般字体貌似跟它的粗体没啥区别,并不是我忘记在它前面加**了)

好了,继续讲。
首先看看这道题:
2 n − 1 2^n-1 2n1个苹果,给你 n n n个箱子(假设它们都足够大或者苹果足够小),怎么放苹果才能使你不管要几个苹果( 0 ≤ 0\le 0 几个 ≤ 2 n − 1 \le2^n-1 2n1),都能成箱成箱地拿(一个箱子都不拿也算成箱成箱地拿)?
对于熟悉二进制的小伙伴们来说,肯定很容易。(好吧,你也许不熟悉但也想出了答案或者你很熟悉但没有想出答案,但这都没关系)

答案

i i i 个箱子123 ⋯ \cdots n n n
2 i − 1 2^{i-1} 2i1个苹果124 ⋯ \cdots 2 n − 1 2^{n-1} 2n1

好了,现在来个升级版:
α \alpha α 个苹果(注意现在 0 ≤ α ≤ ∞ 0\le\alpha\le\infty 0α α \alpha α 不一定正好等于 2 x − 1 2^x-1 2x1),给你 n n n个箱子(假设它们都足够大或者苹果足够小,反正就是不管几个苹果都装得下),怎么放苹果才能使你不管要几个苹果( 0 ≤ 0\le 0 几个 ≤ α \le\alpha α),都能成箱成箱地拿(一个箱子都不拿也算成箱成箱地拿)?
现在对于熟悉二进制的小伙伴们来说,肯定很 “容易”。真的吗?反正别问我,我又不知道。

答案

本题答案:二进制凑不够够?自然数来了!
打几个比方(注意最后不是 2 x 2^x 2x的数):

α \alpha α 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5 6 6 6 7 7 7 8 8 8
分配方案:共 n n n 个箱子(其实这一行无关紧要) 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4
i i i 个箱子 β \beta β β \beta β β \beta β β \beta β β \beta β β \beta β β \beta β β \beta β
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
2 2 2 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
3 3 3 1 1 1 2 2 2 3 3 3 4 4 4 4 4 4
4 4 4 1 1 1

找到算法规律了吗?找不到也没关系。


解释:

对于任意一个自然数 n n n,都有如下的(创建物品)操作:
接着,我们定义操作 f ( n ) f(n) f(n) 为:

  1. 初始化权重为 2 0 2^0 20,也就是 1 1 1
  2. 每次判断权重是否大于 n n n,若为假则执行第 6 6 6 步。
  3. 创建一件物品,重量为 权重 × w i \times w_i ×wi,价值为 权重 × c i \times c_i ×ci
  4. 将权重翻倍(即升一级),然后使 n − n- n 权重。
  5. 回到第 2 2 2 步。
  6. n > 0 n>0 n>0,则创建一件物品,令其重量为 n × w i n\times w_i n×wi,价值为 n × c i n\times c_i n×ci

问题来了(对,还有一个):为什么要这么做?
原因是:
在二进制下(其实应该叫补二进制),无论你想要几件这种物品,你都能从该种物品衍变出来的 “堆” 里面凑足。
事实上,在遍历这些物品的过程中,这个算法就会自动筛选出最合适的数量!


核心代码思路(二进制优化)

维护一个权重 t t t,每次循环之前将它初始化为 2 0 2^0 20,也就是 1 1 1
每输入一种物品和它的个数 s s s,就执行一遍 f ( s ) f(s) f(s)操作,并将操作过程中产生的新物品存入动态数组内,并持续追踪物品总件数。

优化 · 代码

#include <iostream>
#include <vector> // 动态数组
using namespace std;

int main()
{
    int n, m;
    cin >> n >> m;
    vector<int> v{0}, w{0}, dp(m + 1, 0);
    // 这里使用动态数组是因为数据大小的不确定性和对空间的合理利用,以及考虑到初始化方便一点
    /*
        v:price
        w:value
    */
    int vi, wi, si; // 输入变量
    int t; // 维护权重
    int sizen = 0; // 追踪动态数组大小
    for (int i = 0; i < n; ++i) {
        cin >> vi >> wi >> si;
        t = 1; // 初始化权重值
        while (t <= si) { //二进制优化
            v.push_back(vi * t); // 价格乘以权重
            w.push_back(wi * t); // 价值也乘以权重
            si -= t; // 不要忘记去掉权重个数
            t *= 2; // 权重增加一级
            ++sizen; // 增加追踪器
        }
        if (si) { // 该种物品若还有剩余
            v.push_back(vi * si); // 价格乘以剩下物品数
            w.push_back(wi * si); // 价值乘以剩下物品数
            ++sizen; // 增加追踪器
        }
    }
    for (int i = 1; i <= sizen; ++i) // 遍历每一件物品
        for (int j = m; j >= v[i]; --j) // 逆序
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]); // 01背包
    cout << dp[m] << endl; // 输出答案
}

有 “亿” 点点长,但是时间复杂度大大减低至 O ( l o g 2   n ) O(log2\, n) O(log2n),可以应对 s ≤ 2 30 s\le2^{30} s230 的数据范围。


若有任何建议或问题欢迎在评论区指出。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值