题意:
给定一个 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;
}
}