题目描述:
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 非常想得到那只可爱的猫咪,你能帮帮他吗?
Input:
多组输入,每次输入一个 N,表示有 N 个数,之后输入一个长度为 N 的序列 cat, cat[i] <= 1e9 , 3 <= n <= 1e5
sample input:
4
1 3 2 4
3
1 10 2
**
Output:
输出新数组 ans 的中位数
sample output:
1
8
不说思路,谈谈曲折。
好吧,这个图说明了什么懂的自然懂了。
回到这个题,这个题本身的解题思路挺清晰的,找中位数,如果暴力的话,时间和空间都难以接受。那么怎么优化呢?
不是求中位数吗,那么中位数的位置一定在新数组中间的位置也就是(len+1)/2处,这个条件是变不了的,假设中位数是P,只要计算出<=P的数的个数,拿来与mid比较是不是就可以了。
沿着这个思路,怎么找个数呢?不是得有P吗?但是求得就是P啊?怎么弄?
找P!怎么找?枚举。0—10^9枚举?枚举到明天??
所以枚举是不行的,考虑到自然数序列是有序的,P是其中的一个,问题来了。
有序,名次——二分!
这就是具体思路,具体实现需要用到两次二分,第一次也就是枚举P,第二次是运用了枚举的性质,找到最后一个 <=x(枚举变量)的数。
!!!特别注意计算equal_num时一定要放在二分循环结束后!!!一定要放在结束后!!!(说给自己听),要不然大部分数据是能过的,我是写数据生成器(来自hrz大佬)Debug的。。~
具体实现:
接上面,实现一些小细节,计算出最后满足条件的数的时候,往前遍历计算出==x满足的个数,作为一个小技巧,为什么这么做是因为所求中位数所属的数组中的元素的数值不是唯一的,也就是说 0001112322,虽然中位数是一,但是有好多数值上等于中位数的值。
因为咱们枚举P是单一枚举,不可能一个P枚举多次,那样就失去了枚举的意义了。所以枚举的同时计算出P的最小和最大索引,如果包含了(len+1)/2,那么P就是中位数。
代码实现:
#include<iostream>
#include<algorithm>
using namespace std;
int n;
int equal_num;
int cat[100010];
int findj(int i1, int x) {//找到<=X的最后一个位置,记录==x的个数equal_num
int ans = -1, l = i1, r = n;
while (l <= r) {
int mid = (l + r) >> 1;
if (cat[mid] <= x) {
l = mid + 1;
ans = mid;
}
else
r = mid - 1;
}
int index = ans;
while(cat[index] == x && index > 0){//计算equal_num
equal_num ++;
index --;
}
return ans;
}
int main() {
while (~scanf("%d", &n)) {
long int mid = (n * (n - 1) / 2 + 1) / 2;
for (int i = 1; i <= n; ++i) {
scanf("%d", &cat[i]);
}
sort(cat + 1, cat + n + 1);
int lp = 0, rp = cat[n] - cat[1];
while (lp <= rp) {//二分P
int p = (lp + rp) >> 1;
int num = 0;
equal_num = 0;
for (int i = 1; i < n; ++i) {
if (cat[i] + p < cat[i + 1])
continue;
num = num + findj(i + 1, cat[i] + p) - i;
}
if (num >= mid && num - equal_num < mid) {
printf("%d\n", p);
break;
}
else if (num < mid)
lp = p + 1;
else
rp = p - 1;
}
}
return 0;
}
另外还有一种实现,与上述正好相反,但是二分的思路是一样的,上述计算的是中位数之前的数,下面的代码计算的是中位数之后的数,下面没有计算equal_num,所以在二分P的时候left更新世条件弱了-1,整体来说两种方法的复杂度基本一致:
#include<iostream>
#include<algorithm>
using namespace std;
int n;
int number;
int equal_num = 0;
int cat[100010];
int findj(int i1, int x) {//找到第一个<=x的数
int ans = -1, l = i1, r = n - 1;
while (l <= r) {
int mid = (l + r) >> 1;
if (cat[mid] >= x) {
r = mid - 1;
ans = mid;
}
else
l = mid + 1;
}
if (ans == -1)
return n;
return ans;
}
int main() {
while (~scanf_s("%d", &n)) {
int number = n * (n - 1) / 2;
int mid = (number + 1) / 2;
for (int i = 0; i < n; ++i) {
scanf_s("%d", &cat[i]);
}
sort(cat, cat + n);
int lp = 0, rp = cat[n-1] - cat[0];
int p = 0;
while (lp < rp) {
p = (lp + rp) >> 1;
int num = 0;
for (int i = 0; i < n ; ++i) {
num = num + n - findj(i + 1, cat[i] + p);
}
if (num > number - mid)
lp = p + 1;
else
rp = p;
}
cout << lp - 1 << endl;
}
return 0;
}