背包体积为 V(<= 200,000),给出 N(<= 200)个物品,每个物品占用体积为 Vi,价值为 Wi,每个物品要么至多取 1 件,要么至多取 Mi(> 1)件,要么数量无限,求装入背包内物品总价值的最大值。
链接
题解
混合三种背包问题,很经典的一个问题。
首先分开考虑这三种背包问题的解法。
使用动态规划,用 f[v] 表示把所有物品按要求装入一个体积为 v(v <= V)的背包时,装入背包内物品总价值的最大值。
首先,对于 01 背包,显而易见其方程为:
实现代码(t[i].v 和 t[i].w 分别代表 Vi 和 Wi)
for (unsigned int i = 0; i < n; i++) {
for (int v = V; v >= 0; v--) {
if (v >= t[i].v) {
f[v] = std::max(f[v], f[v - t[i].v] + t[i].w);
}
}
}
特别注意第二层循环枚举 v 时的顺序,v 必须从 V 到 0 循环,因为当前 f[v] 要根据一个当 v 更小时的 f[v] 推出(为了腾出大小为 Vi 的空间防第 i 件物品),保证在计算 f[v] 时,f[v - Vi] 一定是没有尝试过放置第 i 件物品时的状态。
对于完全背包,我们可以将其每件拆分成 V / Vi 件 01 背包物品,对每件物品进行一次 01 背包处理。但显然这样做效率会很低。
考虑到完全背包与 01 背包的不同点,仅在于 01 背包每种物品只能放置一次,而完全背包可以放置任意次,将其体现在动态规划的状态转移上,即完全背包问题,需要保证在计算 f[v] 时,f[v - Vi] 一定是已经尝试过放置第 i 件物品时的状态。而只需将第二层循环 v 的遍历顺序改为从 0 到 V 即可。
实现代码为(t[i].v 和 t[i].w 分别代表 Vi 和 Wi):
for (unsigned int i = 0; i < n; i++) {
for (unsigned int v = 0; v <= V; v++) {
if (v >= t[i].v) {
f[v] = std::max(f[v], f[v - t[i].v] + t[i].w);
}
}
}
这两段代码的差异比较难理解,这里举个例子:背包容量 V = 10,仅有一件物品体积 Vi = 3,价值 Wi = 5,现将这件物品尝试放入背包。
如果这件物品是 01 背包:
当 v = 10 时,f[v - Vi] = f[7] = 0,f[v] 被更新为 5。
当 v = 9 时,f[v - Vi] = f[6] = 0,f[v] 被更新为 5。
当 v = 8 时,f[v - Vi] = f[5] = 0,f[v] 被更新为 5。
当 v = 7 时,f[v - Vi] = f[4] = 0,f[v] 被更新为 5。
……
当 v = 4 时,f[v - Vi] = f[1] = 0,f[v] 被更新为 5。
当 v = 3 时,f[v - Vi] = f[0] = 0,f[v] 被更新为 5。
如果这件物品是完全背包:
当 v = 3 时,f[v - Vi] = f[0] = 0,f[v] 被更新为 5。
当 v = 4 时,f[v - Vi] = f[1] = 0,f[v] 被更新为 5。
……
当 v = 6 时,f[v - Vi] = f[3] = 5,f[v] 被更新为 10。
……
当 v = 9 时,f[v - Vi] = f[6] = 10,f[v] 被更新为 15。
当 v = 10 时,f[v - Vi] = f[7] = 10,f[v] 被更新为 15。
以上例子可以体现出 01 背包与完全背包解法上的区别与问题实质的联系。
回到原来的话题上来,我们已经解决了前两类问题——01 背包和完全背包,现在来看多重背包。
还是考虑拆分,把一件可以装 Mi 次的多重背包物品拆分成 Mi 件 01 背包物品,分别对其进行 01 背包处理。这种方法很好理解,但时间复杂度达到了,考虑将其优化。
我们采用类似二进制的思想,将每个多重背包物品拆分为 t 个不同的 01 背包物品,每一个拆分后的物品都有一个系数 k,该物品的体积和价值分别等于原物品的体积和价值乘以这个系数,并且使所有拆分后的物品的系数之和,即原物品最多被放置的次数。并且要使每个系数 k 分别为 ,,,…,,。
举个例子,当 Mi = 17 时,将其拆成 5 件物品,系数 k 分别为 1,2,4,8,2。
使用二进制思想优化过的算法,复杂度降为了。
实现代码为(t[i].v 和 t[i].w 分别代表 Vi 和 Wi):
for (unsigned int i = 0; i < n; i++) {
unsigned int logx = log2(t[i].m), x = 0;
for (unsigned int j = 0; j <= logx; j++) {
x += (1 << j); // 1 << j 将 1 按二进制位左移 j 位,快速计算 2 的 j 次方
for (int v = V; v >= 0; v--) { // 01 背包
if (v >= t[i].v * (1 << j)) {
f[v] = std::max(f[v], f[v - t[i].v * (1 << j)] + t[i].w * (1 << j));
}
}
}
if (x < t[i].m) { // 如果不能 2 的幂作为系数将原物品完全拆分,则多拆分出一件物品 k = Wi - 2 ^ (t - 1)
for (int v = V; v >= 0; v--) { // 01 背包
if (v >= t[i].v * (t[i].m - x)) {
f[v] = std::max(f[v], f[v - t[i].v * (t[i].m - x)] + t[i].w * (t[i].m - x));
}
}
}
}
三种背包问题的思路明确后,就可以考虑混合背包问题了,具体实现方法是对于每一种物品,判断物品类型,分别进行处理。
代码
#include <cstdio>
#include <algorithm>
const unsigned int MAXN = 200;
const unsigned int MAXV = 200000;
unsigned int n, V;
struct thing_t {
unsigned int v, w;
int m;
} t[MAXN];
unsigned int f[MAXV + 1];
inline bool isempty(char ch) {
return ch != '-' && (ch < '0' || ch > '9');
}
template <typename T>
inline void read(T &x) {
x = 0;
register char ch;
while (isempty(ch = getchar()));
register bool flag = false;
if (ch == '-') {
ch = getchar();
flag = true;
}
do {
x = x * 10 + (ch - '0');
} while (!isempty(ch = getchar()));
if (flag) {
x = -x;
}
}
unsigned int log2(unsigned int x) {
unsigned int i = 0, y = 0;
while ((y += (1 << i)) <= x) {
i++;
}
return i - 1;
}
int main() {
read(n), read(V);
for (unsigned int i = 0; i < n; i++) {
read(t[i].v), read(t[i].w), read(t[i].m);
}
register unsigned int result = 0;
for (unsigned int i = 0; i < n; i++) {
if (t[i].m == 1) {
for (int v = V; v >= 0; v--) {
if (v >= t[i].v) {
f[v] = std::max(f[v], f[v - t[i].v] + t[i].w);
result = std::max(result, f[v]);
}
}
} else if (t[i].m == -1) {
for (unsigned int v = 0; v <= V; v++) {
if (v >= t[i].v) {
f[v] = std::max(f[v], f[v - t[i].v] + t[i].w);
result = std::max(result, f[v]);
}
}
} else {
unsigned int logx = log2(t[i].m), x = 0;
for (unsigned int j = 0; j <= logx; j++) {
x += (1 << j);
for (int v = V; v >= 0; v--) {
if (v >= t[i].v * (1 << j)) {
f[v] = std::max(f[v], f[v - t[i].v * (1 << j)] + t[i].w * (1 << j));
result = std::max(result, f[v]);
}
}
}
if (x < t[i].m) {
for (int v = V; v >= 0; v--) {
if (v >= t[i].v * (t[i].m - x)) {
f[v] = std::max(f[v], f[v - t[i].v * (t[i].m - x)] + t[i].w * (t[i].m - x));
result = std::max(result, f[v]);
}
}
}
}
}
printf("%u\n", result);
return 0;
}
吐槽
这段时间在学习 dp,听 liujz 学长讲完后自己抱着书啃了好久 ……
算是有些理解了吧 >_<