题解 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;
}
posted @ 2021-07-08 20:00  Acfboy  阅读(68)  评论(0)    收藏  举报