题解 CF1132E Knapsack
全网都没找到个说得很清楚的正常做法……
很妙的背包。
题目要求的是物品个数很多,背包容量很大但每件物品重量很小,且物品种类也很少时的背包问题。
因为背包的容量实在是太大了,所以不能直接做,我们考虑将物品都尽量打包成一些组,以便组内能够快速地取到最优,同时得满足组外的物品少一点,这样就可以直接对它们做 dp 了。
可以发现,一个组的大小是所有物品的重量的最小公倍数(\(840\))的时候非常合适。因为是最小公倍数,所以这一个组一定可以去满,几个组拼接起来是无缝的,那么取这些组拼满整个大背包就有贪心的最优策略:尽可能多取。
而对于剩下的,每一种物品剩余个数都不可能超过组的大小,即剩下的每种最多 \(840\) 个,加起来不超过 \(840\times 8\), 直接做 dp 就可以了。
令 \(f_{i, j}\) 表示前 \(i\) 种物品剩余的总价值为 \(j\) 的时候能拼成的最多组数。那么枚举每一个剩下 \(k\) 件,设当前的物品拼成 \(840\) 的价值需要的个数是 \(d\), 可以得到转移方程 \(f_{i, j} = \max\{f_{i-1, j-i\times k} + \frac{cnt_{i-1}-k}{d} \}\)。
最后统计答案的时候枚举一个 \(j\) 在加上这最大的个数乘上 \(840\) 即可。
代码(注意其中的 dp 转移方式和说明里的略有不同)。
#include <cstdio>
#include <cstring>
#include <algorithm>
#define int long long
const int N = 10, L = 900;
int f[N][L*N], w, cnt[N];
signed main() {
scanf("%lld", &w);
for (int i = 1; i <= 8; i++)
scanf("%lld", &cnt[i]);
memset(f, -1, sizeof f);
f[1][0] = 0;
for (int i = 1; i <= 8; i++)
for (int j = 0; j <= 840*8; j++) {
if (f[i][j] == -1) continue;
int r = 840 / i;
if (cnt[i] < r) r = cnt[i];
for (int k = 0; k <= r; k++)
f[i+1][j+k*i] = std::max(f[i+1][j+k*i],
f[i][j] + (cnt[i]-k) / (840 / i));
}
int ans = 0;
for (int j = 0; j <= 840*8; j++) {
if (j > w || f[9][j] == -1) continue;
ans = std::max(ans, j + 840*(std::min(f[9][j], (w-j)/840)));
}
printf("%lld", ans);
return 0;
}