【C - TT 的神秘礼物】二分答案

题意:

给定一个 N 个数的数组 cat[i],并用这个数组生成一个新数组 ans[i]。新数组定义为对于任意的 i, j 且 i != j,均有 ans[] = abs(cat[i] - cat[j]),1 <= i < j <= N。试求出这个新数组的中位数,中位数即为排序之后 (len+1)/2 位置对应的数字,’/’ 为下取整。

多组输入,每次输入一个 N,表示有 N 个数,之后输入一个长度为 N 的序列 cat, cat[i] <= 1e9 , 3 <= n <= 1e5。输出新数组 ans 的中位数。

思路:

最暴力的一种方法就是求出新数列,然后进行排序,中位数即为排序之后 (len+1)/2 位置对应的数字。但是复杂度太高,必定超时。

一种思路是二分答案。把数组cat排序,去掉绝对值ans[]=cat[j]-cat[i],假设中位数是P,那么符合cat[j]-cat[i]<=P的(i,j)个数应该是(i,j)总个数的一半。首先对于中位数P进行二分,因为这是绝对值数组所以最小值大于等于0,left=0,最大值应该是排序后的数组的最后一个-第一个,即right=cat[n-1]-cat[0]。遍历i寻找符合条件的j的个数,因为j属于[i+1,n-1],而且数组cat有序,所以可以再一次二分寻找j。

这种方法见代码一,因为要进行两次二分,所以时间开销相对于要大,又超时了。为了降低时间复杂度,采取代码二的方法。

对中位数P进行二分的思想与代码一相同,但是遍历i寻找符合条件的j的个数时不再每次都二分。观察发现,对于固定的i符合条件的j的右边界随着i的向右而向右,呈现一种单调的关系。对于一个固定的i只进行一次二分寻找j的右边界,然后记录这个右边界,后面只需要往后移动右边界指针即可。(思想类似尺取法)

代码:

代码一:

#include<iostream>
#include<algorithm>
using namespace std;
int find(int*& a, int left, int right, int data)//二分j,寻找最后一个小于等于data的数
{
	int i = left - 1;
	int pos = i;
	while (left <= right)
	{
		int mid = (right + left) / 2;
		if (a[mid] <= data)
		{
			pos = mid;
			left = mid + 1;
		}
		else
			right = mid - 1;
	}
	return pos - i;//返回[i+1,pos]的个数
}
int search(int*& a,int n, int left, int right)
{
	int len = (n * (n - 1) / 2 + 1) / 2;//定义len为中位数的名次
	int ans = -1;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		int sum = 0;//求符合a[j]<=a[i]+p的数对个数
		for (int i = 0; i < n; i++)//再一次二分j,使得a[j]<=a[i]+p(j>i)
		{
			int num = find(a, i + 1, n - 1, a[i] + mid);
			sum += num;
		}
		if (sum < len)//p小于中位数
			left = mid + 1;
		else
		{
			ans = mid;
			right = mid - 1;
		}
	}
	return ans;
}
int main()
{
	int n;
	while (cin >> n)
	{
		int* a = new int[n];
		for (int i = 0; i < n; i++)
			cin >> a[i];
		sort(a, a + n);
		int ans = search(a, n, 0, a[n - 1] - a[0]);//0
		cout << ans << endl;
	}
}

代码二:

#include<iostream>
#include<algorithm>
using namespace std;
int find(int*& a, int left, int right, int data)//二分j,寻找最后一个小于等于data的数
{
	int pos = left - 1;
	while (left <= right)
	{
		int mid = (right + left) / 2;
		if (a[mid] <= data)
		{
			pos = mid;
			left = mid + 1;
		}
		else
			right = mid - 1;
	}
	return pos;//返回最后一个满足条件的a[j]的j的值
}
int search(int*& a,int n, int left, int right)
{
	int len = (n * (n - 1) / 2 + 1) / 2;//定义len为中位数的名次
	int ans = -1;
	while (left <= right)
	{
		int mid = (left + right) / 2;//对每一个mid值只进行一次二分,然后双指针遍历
		int sum = 0;//求符合a[j]<=a[i]+mid的数对个数
		int pos = find(a, 1, n - 1, a[0] + mid);
		sum += pos;
		for (int i = 1; i < n; i++)//再一次二分j,使得a[j]<=a[i]+mid(j>i)
		{
			while (pos < n && a[pos] <= a[i] + mid)
				pos++;
			pos--;
			int num = (pos - i > 0) ? pos - i : 0;
			sum += num;
		}
		if (sum < len)//p小于中位数
			left = mid + 1;
		else
		{
			ans = mid;
			right = mid - 1;
		}
	}
	return ans;
}
int main()
{
	int n;
	while (cin >> n)
	{
		int* a = new int[n];
		for (int i = 0; i < n; i++)
			cin >> a[i];
		sort(a, a + n);
		int ans = search(a, n, 0, a[n - 1] - a[0]);
		cout << ans << endl;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值