一、问题描述
已知由n(n≥2)个正整数构成的集合A={ak}(0≤k<n),将其划分为两个不相交的子集A1和A2,元素个数分别是n1和n2, A1和A2中元素之和分别为S1和S2。设计一个尽可能高效的划分算法,满足|n1-n2|最小且|S1-S2|最大。
二、实现思路
在划分算法中,集合A的元素都为正整数,则只需要找到中位数,以中位数为界,将小于中位数的部分归为一个集合,大于等于中位数包括中位数在内的归为另外一个集合。此时,|n1-n2|等于0(数组元素个数为奇数)或1(数组元素个数为偶数),且|S1-S2|最大。很简单的做法就是对数组进行排序,随后输出。但这样基于排序的时间复杂度至少是O(nlogn)(快速排序)。这里注意,由于只需要将数组分割,分割的集合不要求有序,所以只需找到这样一个元素(轴枢),它可以将数组一分为二:该元素之前的元素小于它,该元素之后的元素都大于等于它。如该元素的位置恰好是中央,则划分操作结束。
算法实现具体步骤为:
- 选择轴枢元素,进行一趟划分,得到轴枢元素的准确位置;
- 将轴枢元素位置和数组中央进行比较,如轴枢元素不位于中央,递归处理左(或右)半数组,直至所得元素位置恰好位于数组中央。
对 n+(1/2)n+(1/4)n+(1/8)n+…,取极限得到O(n),则本算法时间复杂度为O(n)。空间复杂度为O(1)。
三、解题代码
#include <iostream>
using namespace std;
#define MaxSize 20000
/*一趟划分*/
int partition(int R[], int low, int high)
{
int temp = R[low]; //以R[low]为基准
while (low<high) //从两端交替向中间扫描
{
while (low < high&&R[high] >= temp) //逆向查找首个小于枢轴的元素
{
high--;
}
R[low] = R[high]; //将小于枢轴的元素交换到低端
while (low < high&&R[low] <= temp)//正向查找首个大于枢轴的元素
{
low++;
}
R[high] = R[low]; //将大于枢轴的元素交换到高端
}
R[low] = temp; //枢轴元素置于正确的位置
return low; //返回枢轴元素的位置
}
/*查找n/2,数组以n/2排序,将序列分成[0...n/2)和[n/2...n)*/
void Divide(int arr[], int n)
{
int low = 0, high = n - 1;
int mid = -1; //[0...mid) [mid...n)
while (low < high)
{
mid = partition(arr, low, high);
if (mid == n / 2) //循环终止条件,当mid位于数组中央时
break;
else if (mid < n / 2) //在右半部分查找
{
low = mid + 1;
}
else //在左半部分查找
{
high = mid - 1;
}
}
}
/*输出数组a[start...end]*/
void OutputArray(int a[], int start, int end)
{
int i;
for (i = start; i <= end - 1; i++)
{
printf("%d ", a[i]);
}
printf("%d\n", a[i]);
}
int main()
{
int n = 0;
int a[MaxSize];
cout << "请输入由正整数构成的集合元素总数: ";
cin >> n;
cout << "请依次输入集合元素: ";
for (int i = 0; i < n; i++)
{
cin >> a[i];
}
Divide(a, n);
cout << "\n划分子集A1:";
OutputArray(a, 0, n / 2 - 1);
cout << " A2:";
OutputArray(a, n / 2, n - 1);
}