PC/UVa:110805/10032
有N
个人拔河,要分成两组,人数最多差1
,使得体重尽量接近,输出两组的体重。N
的最大值是100
,每个人体重最大值450
。
书上提示说枚举其中每一组体重子集实在太多了,所以还是应该找找递推的方法。
最开始我写了一个递推的版本。如果可以计算出i
个人所有组合方式的体重集合,那么对于第i + 1
个人,不加入的话则体重集合没有变化,加入的话那就将已有体重集合中每一个体重加上第i + 1
个人的体重,并和原有的集合合并,就得到了i + 1
个人所有组合方式的体重集合,同时也附带记录每种体重组合方式对应的人数。边界条件vecsSubWeight[0].insert(SubWeight(0, 0))
,表示0
个人时,组合得到体重0
时有0
个人。
vector<set<SubWeight>> vecsSubWeight
用来记录i
个人时体重集合,这样递推结束后,最后一个set<SubWeight>
就是N
个人的所有体重组合。对于100
个人的输入来说,这种方法递推起来非常慢,即使加上人数要小于N / 2
的剪枝也非常慢,100
个人的输入大概要等半小时,同时算法的空间复杂度也很高。
struct SubWeight
{
int weight;
int people;
SubWeight(const int &weight, const int &people) :weight(weight), people(people){};
};
void distribute1(const vector<int> &Weight)
{
int size = Weight.size() / 2;
vector<set<SubWeight>> vecsSubWeight(1, set<SubWeight>());
vecsSubWeight[0].insert(SubWeight(0, 0));
for (size_t i = 1; i <= Weight.size(); i++)
{
vecsSubWeight.push_back(set<SubWeight>());
for (auto iter = vecsSubWeight[i - 1].begin();
iter != vecsSubWeight[i - 1].end(); iter++)
{
vecsSubWeight[i].insert(*iter);
if (iter->people + 1 <= size){
vecsSubWeight[i].insert(SubWeight(iter->weight + Weight[i - 1], iter->people + 1));
}
}
}
const set<SubWeight> &sw = vecsSubWeight.back();
int total = 0, min = MAX_WEIGHT * MAX_N;
int lesser, greater, diff;
for (size_t i = 0; i < Weight.size(); i++)
{
total += Weight[i];
}
for (auto iter = sw.begin(); iter != sw.end(); iter++)
{
if (iter->people == size){
diff = (total - iter->weight) - iter->weight;
if (min > abs(diff)){
min = abs(diff);
if (diff >= 0){
lesser = iter->weight, greater = total - iter->weight;
}
else{
greater = iter->weight, lesser = total - iter->weight;
}
}
}
}
cout << lesser << ' ' << greater << endl;
}
又看了看书上的提示,说体重范围远比子集数量少。观察题目中给的数据范围,可以发现子集体重的变化范围是100 * 450
,这个数值远比C(100, 50)
要小,所以应该换一种存储形式。
使用一个数组记录不同的体重,数组的元素是该达到该体重时,人的组合方式,因为每种体重的组合方式可能不止一种,所以这应该是一个二维数组,但是第二维长度不好确定,所以第二维是变长的,要使用vector
。因为每个人只有选不选两种,所以可以用最多100
个比特位记录组合方式(这就是状态压缩,可以大大降低存储空间,更重要的是位运算很快),这也就是代码中的vector<vector<bitset<MAX_N>>>
。
那么依然是对于第i
个人(从1
计数),遍历第一维,得到当前已经可以组合得到的体重w
,那么新的体重就是w + Weight[i - 1]
(输入体重的下标从0
开始),将旧体重每种组合方式中的第i
位置位,并和新体重的合并(后续体重相同的人可能再次更新此体重,以及这个体重值可能由不同的组合方式得到)。边界条件vecSubWeight[0].push_back(bitset<MAX_N>())
,表示组合得到体重0
时,不选取任何人。
这里要注意体重变量w
的循环一定是从大到小,因为每次合并操作是在更大的体重上操作。如果改成从小到大,在w
时更新了w + Weight[i - 1]
,当w
变化到w + Weight[i - 1]
时会错误的更新w + Weight[i - 1] + Weight[i - 1]
。网上很多博客都没有解释这一点。
这种方法依然很慢,主要是算法有些偏了,题目没有求具体的组合方式,但是递推过程中的第三重循环更新了组合方式。
void distribute2(const vector<int> &Weight)
{
int total = 0;
for (size_t i = 0; i < Weight.size(); i++)
{
total += Weight[i];
}
vector<vector<bitset<MAX_N>>> vecSubWeight(total + 1, vector<bitset<MAX_N>>());
vecSubWeight[0].push_back(bitset<MAX_N>());
for (size_t i = 1; i <= Weight.size(); i++)
{
for (int w = total; w >= 0; w--)
{
if (!vecSubWeight[w].empty()){
for (bitset<MAX_N> bits : vecSubWeight[w - 1])
{
vecSubWeight[w + Weight[i - 1]].push_back(bits);
vecSubWeight[w + Weight[i - 1]].back().set(i);
}
}
}
}
int min = MAX_WEIGHT * MAX_N, size = Weight.size() / 2;
int lesser, greater, diff;
for (int w = 0; w <= total; w++)
{
if (!vecSubWeight[w].empty()){
const vector<bitset<MAX_N>> &vecBits = vecSubWeight[w];
for (auto iter = vecBits.begin(); iter != vecBits.end(); iter++)
{
if (iter->count() == size){
diff = (total - w) - w;
if (min > abs(diff)){
min = abs(diff);
if (diff >= 0){
lesser = w, greater = total - w;
}
else{
greater = w, lesser = total - w;
}
}
}
}
}
}
cout << lesser << ' ' << greater << endl;
}
不用保存组合方式,但是依然需要计算该组合方式的人数。很naive的想法是使用vector<vector<int>>
(或者vector<set<int>>
)来保存每种体重组合人数,但还是无法避免三重循环,因为递推新体重时,需要遍历旧的组合人数,将其中的每个值加1
然后合并,这无论用上面哪种表示形式都比较复杂。
把问题抽象一下:即每次更新时,都会得到一些组合人数,加入到新体重的组合人数中。vector<int>
的合并操作不方便,set<int>
的加1
操作不方便,但是这些数的取值范围就是1
到100
,所以直接用100
个标志位,合并操作使用或运算,这样就避免了三重循环,同样加1
的操作也就变成了左移操作。
使用vector<bitset<MAX_N>>
,第i
比特记录该体重是否可以由i
个人组合得到,这样在扩展新体重时,只需要将原体重的组合方式位向量左移一位,并进行或操作即可。bitset
模板只是看起来复杂,但是其编译后使用位运算,比整数加法要快。
边界条件vecSubWeight[0].set(0)
,表示体重0
由0
个人组合得到。
#include <iostream>
#include <vector>
#include <cmath>
#include <bitset>
#define MAX_N (100 + 1)
#define MAX_WEIGHT 450
void distribute3(const vector<int> &Weight)
{
int total = 0;
for (size_t i = 0; i < Weight.size(); i++)
{
total += Weight[i];
}
vector<bitset<MAX_N>> vecSubWeight(total + 1, bitset<MAX_N>());
vecSubWeight[0].set(0);
for (size_t i = 1; i <= Weight.size(); i++)
{
for (int w = total; w >= 0; w--)
{
if (vecSubWeight[w].any()){
vecSubWeight[w + Weight[i - 1]] |= vecSubWeight[w] << 1;
}
}
}
/*cout << endl;
for (int w = 0; w <= total; w++)
{
if (vecSubWeight[w].any()){
cout << w << ' ';
const bitset<MAX_N> &bits = vecSubWeight[w];
for (size_t pos = 0; pos <= Weight.size(); pos++)
{
if (bits.test(pos)){
cout << pos << ' ';
}
}
cout << endl;
}
}*/
int min = MAX_WEIGHT * MAX_N, size = Weight.size() / 2;
int lesser, greater, diff;
for (int w = 0; w <= total; w++)
{
if (vecSubWeight[w].any()){
const bitset<MAX_N> &bits = vecSubWeight[w];
if (bits.test(size)){
diff = (total - w) - w;
if (min > abs(diff)){
min = abs(diff);
if (diff >= 0){
lesser = w, greater = total - w;
}
else{
greater = w, lesser = total - w;
}
}
}
}
}
cout << lesser << ' ' << greater << endl;
}
int main()
{
int T;
cin >> T;
for (int t = 0; t < T; t++)
{
int N;
cin >> N;
vector<int> Weight(N, 0);
for (int n = 0; n < N; n++)
{
cin >> Weight[n];
}
distribute3(Weight);
if (t != T - 1) cout << endl;
}
return 0;
}
/*
1
3
100
90
200
*/
这样就跟网上能搜到的解法一样了。因为题目要将N
个人分成两组,每一组最多50
人,因此每种体重的组合位向量有51
位就够了,可以使用unsigned long long
。