1113 Integer Set Partition (25 分)
水~。
题意
给定一个由整数组成的集合,集合中的整数各不相同,现在要将它分为两个子集合,使得这两个子集合的并为原集合、交为空集,同时在两个子集合的元素个数n1与n2之差的绝对值|n1-n2|尽可能小的前提下,要求它们各自的元素之和S1与S2之差的绝对值|S1-S2|尽可能大。
思路
首先可以注意到,由于需要先满足两个子集合的元素个数n1与n2之差的绝对值|n1-n2|尽可能小,因此可以知道当原集合中元素个数N为偶数时,两个子集合中的元素个数相同,此时两个子集合的元素个数n1与n2之差的绝对值|n1-n2|为0;而当原集合中元素个数N为奇数时,两个子集合的元素个数n1与n2之差的绝对值|n1-n2|为1,考虑到需要让两个子集合各自的元素之和S1与S2之差的绝对值|S1-S2|尽可能大,因此应该让尽可能多、尽可能大的元素都分配到其中一个子集合中。
由此可以得到最简洁的做法:即把所有元素从小到大排序,然后将较小的N/2(向下取整)个元素作为其中一个集合,而让剩下的元素作为另一个集合,这样就能得到最大的元素和之差,这种做法的时间复杂度为\(O(NlogN)\)。
由于本题的数据范围较小,因此上面的做法是可以通过的,而如果数据范围非常大,那么就有可能超时。此时需要通过快速选择算法找出元素第N/2大,同时根据这个元素把序列分成小于和大于两部分即可。但是同样由于本题数据范围较小,使得rand版本的快速选择算法虽然时间复杂度为\(O(N)\),但其实际效果并不好(rand函数、递归的时间开销都不能忽视),反而会在本题的时间限制下超时。
因此下面介绍一个非常有用的替代函数:nth_element,nth_element函数在头文件algorithm中,需要同时添加“using namespace std;”才能使用,这个函数可以找到序列第K大并把序列分成小于跟大于两部分,也就是说我们可以直接用这个函数来代替随机选择算法,效率颇高。对一个数组a,nth_element(a,a+K,a+N)就是把序列切分成a[0...(K-1)]和a[K...(N-1)]两部分,所以序列第N/2大就可以用nth_element(a,a+N/2,a+N)实现。
代码
直接排序:
const int N=1e5+10;
int a[N];
int n;
int main()
{
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
sort(a,a+n);
int lsum=0,rsum=0;
int mid=a[(n-1)/2];
for(int i=0,j=n-1;i<j;i++,j--)
{
lsum+=a[i];
rsum+=a[j];
}
if(n & 1)
cout<<1<<' '<<rsum+mid-lsum<<endl;
else
cout<<0<<' '<<rsum-lsum<<endl;
//system("pause");
return 0;
}
快速选择算法:
const int N=1e5+10;
int a[N];
int n;
int main()
{
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
nth_element(a,a+n/2,a+n);
int lsum=0,rsum=0;
for(int i=0;i<n/2;i++) lsum+=a[i];
for(int i=n/2;i<n;i++) rsum+=a[i];
cout<<n%2<<' '<<rsum-lsum<<endl;
//system("pause");
return 0;
}