Programming Challenges 习题8.6.5

PC/UVa:110805/10032

Tug of War

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操作不方便,但是这些数的取值范围就是1100,所以直接用100个标志位,合并操作使用或运算,这样就避免了三重循环,同样加1的操作也就变成了左移操作。

使用vector<bitset<MAX_N>>,第i比特记录该体重是否可以由i个人组合得到,这样在扩展新体重时,只需要将原体重的组合方式位向量左移一位,并进行或操作即可。bitset模板只是看起来复杂,但是其编译后使用位运算,比整数加法要快。

边界条件vecSubWeight[0].set(0),表示体重00个人组合得到。

#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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值