题目描述
TT 是一位重度爱猫人士,每日沉溺于 B 站上的猫咪频道。
有一天,TT 的好友 ZJM 决定交给 TT 一个难题,如果 TT 能够解决这个难题,ZJM 就会买一只可爱猫咪送给 TT。
任务内容是,给定一个 N 个数的数组 cat[i],并用这个数组生成一个新数组 ans[i]。新数组定义为对于任意的 i, j 且 i != j,均有 ans[] = abs(cat[i] - cat[j]),1 <= i < j <= N。试求出这个新数组的中位数,中位数即为排序之后 (len+1)/2 位置对应的数字,’/’ 为下取整。
TT 非常想得到那只可爱的猫咪,你能帮帮他吗?
输入
多组输入,每次输入一个 N,表示有 N 个数,之后输入一个长度为 N 的序列 cat, cat[i] <= 1e9 , 3 <= n <= 1e5
输出
输出新数组 ans 的中位数
输入样例
4
1 3 2 4
3
1 10 2
输出样例
1
8
思路
先看数据规模,n最大能取到1e5,因此我们肯定是接受不了 O ( n 2 ) O(n^2) O(n2)的做法。而算出所有的ans数组元素,然后通过遍历寻找中位数的值,复杂度正是 O ( n 2 ) O(n^2) O(n2),这我们无法接受。既然求答案很困难,我们可以考虑是否可以验证答案,这是可以的!
中位数是个整数,并且我们很容易求出中位数的取值范围(0~(max(cat) - min(cat)))。而且,这是一个单调问题:对于取值范围内的每一个数,我们可以求出它在ans数组中的名次,根据这个名次,我们就能确定中位数是小于这个数,还是大于这个数,再或是等于这个数。这也就意味着,我们可以根据名次,来二分查找得到中位数。
于是,现在的问题只剩下一个,那就是如何计算名次呢?毕竟如果直接计算ans数组会超时。
看来,我们需要对问题进行一定的转化。对于一个数P,寻找其在ans数组中的名次,也就是寻找有多少i, j(1 <= i < j <= N),满足
∣
c
a
t
i
−
c
a
t
j
∣
<
P
|cat_i-cat_j|<P
∣cati−catj∣<P。我们想对这个式子变形,就不可避免的要去掉绝对值。为了方便去掉绝对值,我们可以提前将cat数组排好序(如从小到大排序)。于是就可以得到:
c
a
t
j
<
c
a
t
i
+
P
cat_j < cat_i + P
catj<cati+P。于是,我们便可以枚举所有的cat_i,寻找满足条件的cat_j的数量,而寻找cat_j数量的过程,我们又可以通过二分来实现。
因此,计算一个数名次的复杂度为
O
(
n
l
o
g
(
n
)
)
O(nlog(n))
O(nlog(n)),假设中位数的取值范围是0~m,则总复杂度为
O
(
n
l
o
g
(
n
)
l
o
g
(
m
)
)
O(nlog(n)log(m))
O(nlog(n)log(m)),该思路可行。
注意
本题步骤较多,最好分清层次,逐步完成。
完整代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n;
int a[100005];
int findcnt(int i, int xi_p)
{
int l=i+1, r=n;
int m;
if(a[l] > xi_p) return 0;
while(l < r)
{
m = (l+r)>>1;
if(a[m] <= xi_p) l = m+1;
else r=m;
}
return l - (i+1);
}
bool judge(int p, int index) // x的名次小于index则返回1, 反之返回0
{
int sum=0;
for(int i=0;i<n-1;i++)
{
sum += findcnt(i, a[i]+p);
}
return sum < index;
}
int find(int index)
{
int l=0, r=a[n-1] - a[0];
int m;
while(l<r)
{
m = (l+r)>>1;
if(judge(m, index)) l = m+1;
else r = m;
}
return l;
}
int main ()
{
while(~scanf("%d", &n))
{
for(int i=0;i<n;i++) scanf("%d", &a[i]);
int indexOfMid = ((n*(n-1) >> 1) + 1) >> 1;
sort(a, a+n);
printf("%d\n", find(indexOfMid));
}
return 0;
}