问题描述:
给定一个 N 个数的数组 cat[i],并用这个数组生成一个新数组 ans[i]。新数组定义为对于任意的 i, j 且 i != j,均有 ans[] = abs(cat[i] -cat[j]),
1 <= i < j <= N。试求出这个新数组的中位数,中位数即为排序之后 (len+1)/2 位置对应的数字,‘/’ 为下取整。
input:
多组输入,每次输入一个 N,表示有 N 个数,之后输入一个长度为 N 的序列 cat, cat[i] <= 1e9 , 3 <= n <= 1e5
output:
输出新数组 ans 的中位数
样例输入:
4
1 3 2 4
3
1 10 2
样例输出:
1
8
解题思路:
首先我们可以考虑暴力做法,枚举i和j,得到数组ans,然后求中位数,但是复杂度为 n 2 n^2 n2,不能接受。那么,如果我们已知了一个答案的区间,并且这个区间是单调的,那么我们就可以二分答案,对每一次二分得到的数的名次与中位数的名次比较,如果名次小于中位数,则二分值比中位数要小,如果名次大于中位数,则二分值比中位数要大,如果名次等于中位数,那它就是中位数。所以我们可以通过二分答案的方法,来降低算法复杂度。
如果我们得到了一个二分值P,怎么去求它的名次呢,如果在数组ans中求,那么和暴力方法没有区别,考虑到有绝对值,首先我们可以对cat数组进行排序,去掉绝对值号,那么ans中的数组元素就是cat[j]-cat[i],其中i<j。在求P的名次时,就是求满足cat[j]-cat[i]<=P的所有二元组对数。通过移项,得cat[j]<=P+cat[i],那么我们可以通过枚举下标i求出满足条件的下标j的个数,从而得到P的名次,在求下标j的个数时,由于cat数组已经有序,我们也可以通过二分来找到数组中最后一个大于等于P+cat[i]的位置,从而得到j的最大值,并且j>i,所以名次等于每一次j的最大值减去i的累加和。在求出名次之后,和中位数名次进行比较,继续二分。这样通过两次二分,就能找到中位数,算法的复杂度为 n l o g 2 n nlog^2n nlog2n。
由于得到的新数组ans中可能有重复值,按照这种算法来计算,即使它是中位数,它的名次依然有可能大于中位数的名次,所以在进行二分时,名次大于等于中位数的名次当成一种情况处理。
代码:
#include<iostream>
#include<cstdio>
#include <algorithm>
#include<string.h>
#include<vector>
using namespace std;
int search2(int x,int left,int right,int ca[])
//查找最后一个大于等于P+cat[i]的位置
{
int ans=-1;
while(left<=right)
{
int mid=(left+right)/2;
if(ca[mid]<=x)
{
ans=mid;
left=mid+1;
}
else
right=mid-1;
}
return ans;
}
int search(int left,int right,int ca[],int number)
{//求解中位数,采用二分
int to=(number*(number-1)/2+1)/2;//中位数的位置
int ans=-1;
while(left<=right)
{
int rank=0;
int mid=(left+right)/2;
for(int i=0;i<number;i++)//枚举所有i,找到符合条件的j
{
int temp=ca[i]+mid;
int num=search2(temp,0,number-1,ca);
if(num!=-1)
rank+=num-i;
}
if(rank<to)
left=mid+1;
else
{//rank>=to当成一种情况,具体原因思路里提到过了
ans=mid;
right=mid-1;
}
}
return ans;
}
int main()
{
int n;
while(scanf("%d", &n) != EOF)
{
int cat[100001];
for(int i=0;i<n;i++)
scanf("%d",&cat[i]);
sort(cat,cat+n);//排序
int middle=search(0,cat[n-1]-cat[0],cat,n);
cout<<middle<<endl;
}
}