一、题意
有6堆石头,第i堆石头中每个石头的价值都是i。然后输入每堆石头的数量num[i]。
要求你判断是否可以将所有石头分成总价值相等的两堆。
二、解析
首先算出所有石头的总价值V,若V为奇数,则无解。
若V为偶数,则我们的目标就是拿一个体积为V/2的背包,然后尽可能装满它。若装得满,说明原问题可以分成价值相等的两堆,否则不能。
由于每种价值的石头有数量限制,因此这是一个多重背包问题。
在本题对应的背包问题中,物体(也就是石头)的价值和体积都是V[i]。于是我们套用完全背包的模板即可。
一般情况下,完全背包问题使用二进制法就基本不会超时了,它的原理就是将一个数量为p[i],价值为v[i]的物体拆分成 1、2、4、8...这若干份(每一份右2的幂次份原物体构成,构成的新物体价值为kw[i],体积为kv[i]),然后注意最后不足2的幂次的数量也单独做成1份,这样物体的总数量就降为了 O(∑lgn)。然后就可以按照01背包问题的套路来处理这∑lgn个新物体了。
正确性是由二进制原理保证的:因为原来可能选择的1~p[i]个原物体一定可以被替换为新物体的组合。
三、代码
#include <iostream> // #include <cmath> #define max(x, y) (x)>(y)?(x):(y) #define min(x, y) (x)<(y)?(x):(y) using namespace std; const int n = 6, maxv = 210000 + 5; const int v[n] = {1, 2, 3, 4, 5, 6}; int kase = 1, p[n], dp[maxv]; void output(bool ok) { cout << "Collection #" << kase ++ << ":" << "\n"; cout << (ok ? "Can be divided.\n" : "Can't be divided.\n") << endl; } int main() { while(cin >> p[0] >> p[1] >> p[2] >> p[3] >> p[4] >> p[5]) { int V = 0; for(int i = 0; i < n; i ++) V += v[i] * p[i]; if(V == 0) break; else if(V % 2) output(0); else { V /= 2; fill(dp, dp + V + 1, 0); for(int i = 0; i < n; i ++) { for(int k = 1, num = min(p[i], V / v[i]); num > 0; k <<= 1) { if(k > num) k = num; num -= k; for(int j = V; j >= k * v[i]; j --) { dp[j] = max(dp[j], dp[j - k * v[i]] + k * v[i]); } } } output(dp[V] == V); } } }
四、归纳
- 多重背包问题:将n种物品放入体积为V的背包中,每种物体有数量限制p[i]。二进制法实现的模板如下( 用时O(V*∑lg(p[i])) ):
int v[n], w[n], p[n], dp[maxv]; // p[i]表示原物体i的数量限制 fill(dp, dp + V + 1, 0); for(int i = 0; i < n; i ++) { for(int k = 1, num = min(p[i], V / v[i]); num > 0; k <<= 1) { // 每一个新物体由k个原物体组成 if(k > num) k = num; // 最后不足2的幂次的单独一份 num -= k; for(int j = V; j >= k * v[i]; j --) { // 对新物体使用01背包问题处理,这里使用的是01背包问题的滚动数组模板,当然也可以不用 dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]); } } }
- 01背包问题可以通过滚动数组的形式来减少空间复杂度,只需要将j的遍历顺序反过来即可。但注意,如果需要打印路径的话,就不能使用滚动数组。
fill(dp, dp + V + 1, 0); for(int i = 0; i < n; i ++) { for(int j = V; j >= v[i]; j --) { dp[j] = max(dp[j], dp[j - v[i]] + w[i]); } }
- 实际上还有一种使用单调队列的优化方式,但由于技巧性比较高,一般用不到