写在前面
其实在学习了01背包问题与完全背包问题的求解与优化后,接下来与背包问题有关的问题基本都可以用这两种问题的思想去实现,因此如果没看过我之前写的这两篇博客的话可以点击下方的相关链接查看。
本文我们将在这两个问题的基础上,继续探讨多重背包问题的求解与其中一种优化(另一种比较复杂的优化将在背包问题进阶篇中讲解)。
多重背包问题
原题链接:https://www.acwing.com/problem/content/4/
基本描述
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi ,价值是 wi 。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量且价值总和最大,并将最大的价值输出。
输入格式第一行两个整数 N,V 用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi , wi , si 用空格隔开,分别表示第 i 种物品的体积、价值和数量。
数据范围0 < N , V ≤ 1000 < N , V ≤ 100
0 < vi , wi , si ≤ 1000 < vi , wi , si ≤ 100
输入样例4 5 1 2 3 2 4 1 3 4 3 4 5 2
输出样例
10
题意解析
接下来我们以测试样例为例来理解一下本题的意思,在本题中给出的数据有:
-
物品的数量N(4)
-
背包的容量V(5)
-
每件物品的体积、价值与数量(下面以表格的形式呈现)
体积 | 价值 | 数量 | |
---|---|---|---|
物品1 | 1 | 2 | 3 |
物品2 | 2 | 4 | 1 |
物品3 | 3 | 4 | 3 |
物品4 | 4 | 5 | 2 |
由此我们可以看出,本题实际上是对01背包问题的一种变式(为什么是01背包问题而不是完全背包问题请继续往下看),只不过01背包问题中每种物品只有选与不选两种决策,而多重背包问题则可以在背包容量以及物品数量允许的情况下拥有不止一种的决策,而其与完全背包问题的差别则在于它除了受到背包容量的约束外,还受到物品数量的限制。
对于本题,我们可以得知,背包容量为5
,可能的选择中有:
- 物品1的体积为1,价值为2,数量为3;物品2的体积为2,价值为4,数量为1;我们可以选择要全部三个物品1和一个物品2,以获取总价值10
- 物品2的体积为1,价值为4,数量为1;物品3的体积为3,价值为4,数量为3;我们可以选择要一个物品2和一个物品3,以获取总价值8
对多种符合条件的可能性进行分析计算,最终得出所能获取的最大价值为10。
思路点拨
在思路点拨的部分,我们来看一下为什么说多重背包问题更像是01背包问题的延伸,而不是完全背包问题。
首先,我们知道01背包问题只有选与不选两种决策,这是对于可供选择的每一件物品来说的。
那么,我们是否可以把多重背包问题想成是在01背包问题的条件里有多个体积和价值一样的物品呢?
很显然,这是可以的。也就是说,我们可以把题中给出的物品信息
体积 | 价值 | 数量 | |
---|---|---|---|
物品1 | 1 | 2 | 3 |
物品2 | 2 | 4 | 1 |
物品3 | 3 | 4 | 3 |
物品4 | 4 | 5 | 2 |
看成是01背包问题下多个相同的物品的集合。那么如果换成01背包问题的条件,就应该为:
体积 | 价值 | |
---|---|---|
物品1.1 | 1 | 2 |
物品1.2 | 1 | 2 |
物品1.3 | 1 | 2 |
物品2 | 2 | 4 |
物品3 | 3 | 4 |
… | … | … |
而完全背包问题显然不能这样转换,因为每一件物品的数量都是无限的。在有限和无限两种情况下,我
们对同一道问题就会有不同的解法,这就是01背包与完全背包的差别。
理解了这一点后,我们接下来的问题就变得简单很多,在不考虑时间复杂度的情况下,我们仅需在01背包问题的基础上多加一层循环用于限制物品数量的变化即可。
由于多重背包问题可以看成是01背包问题的延伸,我们依旧可以用一维数组来代替原本的二维数组实现空间复杂度的优化,不明白优化原理的同学可以自行前往我的博客:背包问题入门 #1 | 01背包问题的求解与优化中的优化部分了解。为了不让博客过于冗长,从本题开始,我将直接用一维数组来解题,希望大家理解。
对于本题,我们定义
- 两个变量
n,m
来获取物品的数量以及背包的容量; - 三个变量
v,w,s
分别获取每个物品的体积、价值与数量,在循环内部边更新边处理; - 一个一维数组
dp[j]
表示背包的容量为j
时所能获取的最大价值。
代码实现
C++代码实现如下:
带注释版:
#include <iostream>
using namespace std;
const int N = 110; // 数组的大小,取值稍大于数据范围即可
int n, m; // 变量 n,m 来获取物品的数量以及背包的容量
int f[N]; // 一维数组 dp[j] 表示背包的容量为 j 时所能获取的最大价值
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++) // i 循环让考虑物品的数量不断增加
{
int v, w, s; // 三个变量 v,w,s 分别获取每个物品的体积、价值与数量,在循环内部边更新边处理
cin >> v >> w >> s;
for (int j = m; j >= v; j--) // j 循环让背包的最大容量不断增加
{
for (int k = 1; k <= s && k * v <= j; k++)
// k 循环让选用当前物品的数量不断增加,k不能大于物品最大数量s,k * v不能大于当前循环的背包容量 j
{
f[j] = max(f[j], f[j - v * k] + w * k);
}
}
}
cout << f[m] << endl;
return 0;
}
纯净版:
#include <iostream>
using namespace std;
const int N = 110;
int n, m;
int f[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
int v, w, s;
cin >> v >> w >> s;
for (int j = m; j >= v; j--)
{
for (int k = 1; k <= s && k * v <= j; k++)
{
f[j] = max(f[j], f[j - v * k] + w * k);
}
}
}
cout << f[m] << endl;
return 0;
}
多重背包问题优化的必要性
对于本题来说优化是十分必要的,因为我们之所以能用三重循环暴力破解此题,是因为题目中已经明确说明了数据范围小于100,若数据范围再大一点,很可能会导致运行时间过长以至出现超时错误。
由于篇幅限制,在本文的优化部分,将只对多重背包问题的二进制优化进行讲解,其他的优化办法将在后续的博客中继续为大家讲解,敬请期待。
多重背包问题的二进制优化实际上可以看成是一道更改了数据范围的题,原题链接为:https://www.acwing.com/problem/content/5/,由于它只是将数据范围从原先的100改为1000,在本文中将不再具体展示。
多重背包问题的二进制优化
在思路点拨中,我们说多重背包问题可以看成是01背包问题的延伸,也就是01背包问题在具有多件相同物品时的情况,由于题中给出物品1的数量为3
,因此表格就变成了:
体积 | 价值 | |
---|---|---|
物品1.1 | 1 | 2 |
物品1.2 | 1 | 2 |
物品1.3 | 1 | 2 |
物品2 | 2 | 4 |
物品3 | 3 | 4 |
… | … | … |
为了让大家更直观地理解二进制优化的合理性,我们不妨假设物品1的数量为10
,那么表格就会变成:
体积 | 价值 | |
---|---|---|
物品1.1 | 1 | 2 |
物品1.2 | 1 | 2 |
物品1.3 | 1 | 2 |
… | … | … |
物品1.10 | 1 | 2 |
物品2 | 2 | 4 |
物品3 | 3 | 4 |
… | … | … |
这样看起来物品的数量就十分多了,如果我们循环判断每一件单个的物品,时间复杂度显然会变得很大,这也是为什么多重背包问题需要优化的原因。
但真的有必要让每一个物品1作为一个单独的物品在表格中罗列吗?
我们是否有可能将物品呈现的方式合理优化,但不影响最终的计算结果呢?
结果是肯定的。因为深入思考后我们可以发现将三个物品看成一个整体考虑和把它们分开三个考虑的效果是一样的,我们相当于可以把体积1、价值2
的物品1看作是体积3、价值6
的一个新物品(即三个物品1的组合),并将其作为一个单独的物品放在表格中考量。
那么接下来,我们需要解决的问题就变成了:如何巧妙地把多个相同物品组合成若干个该物品的小集合,并且不影响最后的结果。
我们回想之前学过的进制转换的内容,任何一个十进制数字都可以转化成二进制,例如15D = 1111B、10D=1010B
等,也就是说,我们可以把15等效看成是8、4、2、1的组合;10等效看成是8、2的组合。
但如果具体给出了一个数字,比如我要求只需表示10以内的所有数字,则不一定需要用到8,仅需10-4-2-1=3
就可以实现,由于我的数学能力有限,本文将不给出具体的证明,有兴趣的同学可以自行查询相关证明,下面针对10这个数字列出具体的表示方案。
1到10以内的数字用3 4 2 1四位数字(8 4 2 1的演化,因为仅需3即可表示所有,做到进一步优化)表示:
- 1 = 1
- 2 = 2
- 3 = 1 + 2
- 4 = 4
- 5 = 1 + 4
- 6 = 2 + 4
- 7 = 1 + 2 + 4
- 8 = 1 + 3 + 4
- 9 = 2 + 3 + 4
- 10 = 1 + 2 + 3 + 4
可以看出,10以内的数字均可以用3 4 2 1四位数中的若干位组合表示出来,也就是说,我们可以把上面的表格转换为:
体积 | 价值 | |
---|---|---|
物品1(1) | 1 | 2 |
物品1(2) | 2 | 4 |
物品1(4) | 4 | 8 |
物品1(3) | 3 | 6 |
物品2 | 2 | 4 |
物品3 | 3 | 4 |
… | … | … |
可能有些同学还是有点疑惑,我们再来举几个例子。
-
假如有一种决策需要我们选择5个物品1,显然,我们可以用物品1(1)和物品1(4)来实现;
-
假如有一种决策需要我们选择8个物品1,显然,我们可以用物品1(1)、物品1(3)和物品1(4)来实现。
我们可以发现,在动态规划状态转移的过程中每一种有关物品1的选择都可以通过上述四种物品1的集合的组合来实现,这也就说明了:多重背包问题的二进制优化是可行的。
优化后的代码实现
C++代码实现如下:
带注释版:
#include <iostream>
#include <vector> // 用到了 vector 容器,需包含头文件
using namespace std;
const int N = 2010; // 数组的大小,取值稍大于数据范围即可
int n, m;
int f[N];
struct Good // 定义一个结构体 Good,用于组装相关物品成为集合,组装结果放在vector容器中,作为一个独立的选择
{
int v, w;
};
int main()
{
vector<Good>goods; // 定义一个 vector 容器用于存放优化后的物品选择
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
int v, w, s;
cin >> v >> w >> s;
for (int k = 1; k <= s; k *= 2) // 若物品的数量不为1,则可以对其进行二进制优化
{
s -= k; // 与进制转换的道理类似,每次拿了之后从总数中减去
goods.push_back({ k * v,k * w }); // 将组装后物品集合的体积与价值作为一个新的物品放到容器中
}
if(s > 0) goods.push_back({ s * v,s * w });
// 将剩余的物品数量组装成最后一个集合放到容器中,例如10=3+4+2+1,这里就是对3的组装
}
for (auto it : goods) // 循环遍历 vector 容器,相当于之前对 i 的循环,只不过之前进行了组装,所以需要对容器进行遍历
{
for (int j = m; j >= it.v; j--) // 参考01背包问题的实现,很好明白
{
f[j] = max(f[j], f[j - it.v] + it.w);
}
}
cout << f[m] << endl;
return 0;
}
纯净版:
#include <iostream>
#include <vector>
using namespace std;
const int N = 2010;
int n, m;
int f[N];
struct Good
{
int v, w;
};
int main()
{
vector<Good>goods;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
int v, w, s;
cin >> v >> w >> s;
for (int k = 1; k <= s; k *= 2)
{
s -= k;
goods.push_back({ k * v,k * w });
}
if(s > 0) goods.push_back({ s * v,s * w });
}
for (auto it : goods)
{
for (int j = m; j >= it.v; j--)
{
f[j] = max(f[j], f[j - it.v] + it.w);
}
}
cout << f[m] << endl;
return 0;
}
参考资料
【1】崔添翼 . 背包问题九讲