前言
没有理解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 v≤100,w≤1000,s≤10
朴素 · 思路
输入部分注意区分字母,在此就不多作赘述了。
好了,这里开始讲思路。
最容易想到的算法是把每件物品的数量拆成循环然后当成01背包做。
但是这个算法和多重背包差不多,都要注意几点:
- 在状态转移方程内,要把物品的重量和价值扩大 k k k 倍(不明确指出时, k k k 默认为物品件数的循环变量)。
- 注意下限为 0 0 0。
- 注意上线的判断终止条件。
上限
上限现在具有两个判断条件:
- k × w i ≤ j k\times w_i\le j k×wi≤j
- k ≤ s i k\le s_i k≤si
此处 s i s_i si 为该物品的数量。
其实还可以这么写:
k
≤
m
i
n
(
j
÷
w
i
,
s
[
i
]
)
k\le min(j\div w_i,s[i])
k≤min(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
2n−1个苹果,给你
n
n
n个箱子(假设它们都足够大或者苹果足够小),怎么放苹果才能使你不管要几个苹果(
0
≤
0\le
0≤ 几个
≤
2
n
−
1
\le2^n-1
≤2n−1),都能成箱成箱地拿(一个箱子都不拿也算成箱成箱地拿)?
对于熟悉二进制的小伙伴们来说,肯定很容易。(好吧,你也许不熟悉但也想出了答案或者你很熟悉但没有想出答案,但这都没关系)
答案
第 i i i 个箱子 | 1 | 2 | 3 | ⋯ \cdots ⋯ | n n n |
---|---|---|---|---|---|
装 2 i − 1 2^{i-1} 2i−1个苹果 | 1 | 2 | 4 | ⋯ \cdots ⋯ | 2 n − 1 2^{n-1} 2n−1 |
好了,现在来个升级版:
有
α
\alpha
α 个苹果(注意现在
0
≤
α
≤
∞
0\le\alpha\le\infty
0≤α≤∞,
α
\alpha
α 不一定正好等于
2
x
−
1
2^x-1
2x−1),给你
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) 为:
- 初始化权重为 2 0 2^0 20,也就是 1 1 1。
- 每次判断权重是否大于 n n n,若为假则执行第 6 6 6 步。
- 创建一件物品,重量为 权重 × w i \times w_i ×wi,价值为 权重 × c i \times c_i ×ci。
- 将权重翻倍(即升一级),然后使 n − n- n− 权重。
- 回到第 2 2 2 步。
- 若 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} s≤230 的数据范围。
若有任何建议或问题欢迎在评论区指出。