随机选择算法应用

点击查看代码
/*
问题:
给定一个由整数组成的集合,集合中的整数各不相同,现在要将它分为两个子集合,使得这两个子集合的并为原集合,交为空集,同时在两个子集合的元素个数n1与n2
之差的绝对值|n1-n2|尽可能小的前提下,要求它们各自的元素之和S1与S2之差的绝对值|S1-S2|尽可能大。求这个|S1-S2|等于多少
输入样例:
13
1 6 33 18 4 0 10 5 12 7 2 9 3

输出样例:
80
*/

/*
题目分析:
如果原集合的元素个数n为偶数时,由它分出的两个子集合的元素个数都是n/2。如果n是奇数,则两个子集合的元素个数分别是n/2和n/2+1(除法是向下取整)
为了使|S1-S2|尽可能大,最直接的方法是将原集合的元素从小到大排序,取排序后的前n/2个元素作为一个子集合A1,其余元素作为另一个子集合A2,时间复杂度O(nlogn)

更优的方法:
使用随机选择算法。因为题目没有要求子集合内的元素需要排序,因此只需要找到原集合中第n/2大的元素a[p],将小于a[p]的元素都放入A1,大于等于a[p]的元素都放入A2,
期望时间复杂度O(n)
*/

#include<cstdio>
#include<stdlib.h> //使用srand()和rand()
#include<time.h> //使用time()
#include<cmath> //使用round()
#include<algorithm> //使用std::swap()
#pragma warning(disable:4996)
const int maxn = 100010; //最多10^5个数
int A[maxn], n; //A[]存放原始集合的元素,n是A[]的元素个数

//选取随机分界点,对区间[left,right]进行划分
int randPartition(int A[], int left, int right) {
	//生成[left,right]范围内的随机数p
	int p = (int)(round(1.0 * rand() / RAND_MAX * (right - left) + left));
	std::swap(A[p], A[left]); //交换A[p]和A[left],将随机数A[p]作为分界点A[left]

	//快速排序
	int temp = A[left];	//将A[left]存入temp中,防止在排序过程中覆盖A[left]的原始数据
	while (left < right) {
		while (left < right && A[right] > temp) right--; //左移right,当遇到小于等于分界点temp的元素时,将该元素放在分界点左边
		A[left] = A[right];	//小于等于temp的元素放到分界点左边
		while (left < right && A[left] <= temp) left++;	//右移left,当遇到大于分界点temp的元素时,将该元素放在分界点右边
		A[right] = A[left];	//大于temp的元素放到分界点右边
	}
	A[left] = temp;	//left=right时结束循环,将分界点放在A[left]上,此时它左边的元素都小于等于它,右边的元素都大于它
	return left; //返回分界点位置
}

//随机选择算法,从A[left,right]中找到第K大的数,并对区间进行划分
int randSelect(int A[], int left, int right, int K) {
	if (left == right) return A[left]; //到达边界返回
	int p = randPartition(A, left, right); //划分后分界点的位置p
	int M = p - left + 1; //A[p]是A[left,right]中的第M大个数
	if (K == M) return A[p]; //找到第K大的数就返回
	else if (K < M) { //K<M,第K大的数在分界点左侧[left,p-1]
		return randSelect(A, left, p - 1, K);
	}
	else { //K>=M,第K大的数在分界点右侧[p+1,right]
		return randSelect(A, p + 1, right, K - M); //因为A[p+1]是第M+1大的数,因此第K大的数在[p+1,right]内是第K-M大的数
	}
}

int main() {
	srand((unsigned)time(NULL)); //生成随机数的种子
	//sum是原始序列的所有整数之和,sum1是存储前n/2大的元素的序列的所有整数之和
	int sum = 0, sum1 = 0;
	scanf("%d", &n);
	for (int i = 0; i < n; i++) {
		scanf("%d", &A[i]);
		sum += A[i]; //累计原始序列的所有整数之和
	}
	randSelect(A, 0, n - 1, n / 2); //查找A[]中第n/2大的数(假设记为A[p]),将小于等于A[p]的数放在它的左边区间,大于A[p]的数放在它的右边区间
									//左边区间的所有元素形成集合A1,右边区间的所有元素形成集合A2
	for (int i = 0; i < n / 2; i++) {
		sum1 += A[i]; //累计A1集合的所有元素之和
	}
	printf("%d\n", (sum - sum1) - sum1); //求A2和A1集合的元素和之差(A2集合的所有元素之和=sum-sum1)
	return 0;
}
posted @ 2022-09-30 22:58  zhaoo_o  阅读(17)  评论(0编辑  收藏  举报