在开始所有问题之前,先搞清二分是什么非常重要,只有熟练掌握二分的基础知识才能在二分算法的基础上,理解二分答案算法并求解相应的问题。
由于二分的讲解需要篇幅较大,在此我们引入隔壁园的一篇超详细的讲解,希望你先掌握了扎实的二分算法基础再开始本篇的阅读:这里是传送门
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 位置对应的数字,’/’ 为下取整。
多组输入,每次输入一个 N,表示有 N 个数,之后输入一个长度为 N 的序列 cat, cat[i] <= 1e9 , 3 <= n <= 1e5
输出新数组 ans 的中位数
4
1 3 2 4
3
1 10 2
1
8
题目不算抽象,十分容易理解,但越是这样,挖的坑才越深
有一个整数数组,现在要生成一个数组ans[],且满足其中的元素满足:
ans[] = abs(cat[i] - cat[j])
任务时求解这个新数组的中位数大小,数据大小是1e5
也许看到题目的第一眼,你会认为哎,这都什么题,太水了
然后算都不算复杂度,直接暴力上手就成功TLE掉
回想下暴力的做法,处理n个点,在生成新数组ans[]时使用了O(n^2)的时间复杂度,这在题目要求的1ms时间内是不可能完成的。那可能你会奇怪,这怎么优化啊????
难点一:引入二分函数并判断其正确性
这时候我们就需要引入二分答案法,来完成不计算生成数组求出中位数。
经过前面的学习,你应该了解了:只要区间内走势是单调的,就可以二分
在该题目中,数组值是不能直接求解的,所以数组值的单调性不可利用,我们需要寻求一种指标用来代替数组的单调性并进行二分的求解。
在计算中位数时,数组值是单调递增的,那么排名在前面的元素值也一定高于后面,经过这种推理我们就可以将数组值的二分转换成了名次的二分。
但受限于ABS()函数的非单调性,不可直接进行名次的求解,我们采用先sort()排序保证当前数组单调性,再求解排名的方式,这就保证了参与计算的所有元素都具有一致的单调性,可以使用排名开始二分。
难点二:求解排名
由于目标数列不已知,排名的求解不能采用传统的排序方法进行。在上述的算法求解过程中,每得到一个数字a并求解其排名,实质上可以表示为
{对(i,j)求和 | i,j满足xj-xi<=a}
解决这个问题,可以对i进行枚举,在一次求解循环中,将i视为一个定值,a也为一个定值,该算法就可以转换为求解满足条件的j出现的最后位置,由于数组已经有序单调,我们可以再次使用整数二分求解。至此,解题过程中全部问题被解决,该题目难点消除。
易错点
虽然题目的难点已经被我们剖析开来,但是实现过程中还是容易在一些易错点上踩坑。第一遍实现这道题目,调参可能会是很痛苦的一件事,具体的参数分析详解见代码及相关的备注
int search_mid(int num)//二分答案实质:对整个答案区间进行二分查找,在不知道每个具体值的情况下,利用其他的一些推导函数(例如名次)来进行查找
{
int left=0;
int right=a[num-1]-a[0];
int mid=0;
int ans=-1;
int mid_pos=(num*(num-1)/2+1)/2;
while(right>=left)
{
int rank=0;
mid=(left+right)/2;
for(int i=0;i<num;i++)
{
int flag=search_j(a[i]+mid,0,num-1);
if(flag!=-1)
rank+=(flag-i);
}
if(rank>=mid_pos)
//这里用大于等于的原因是,某个数名次等于中位数,但是实际上可能并不是中位数
//因为这里的rank找的是有多少组i-j<=p,如果找的这个P在新的数列中并不存在,他等于队列中上一个比他小的数的名次
//例如新队列 4 4 4 8 8 12,找5,6,7,得到的rank都等于3,但是其实5 6 7 都不是中位数,公式:任何一个数,只要>n,
//如果在队列 4 4 4 8 8 12中找中位数,6的rank==3,但是仍然需要向前部分找---
//根本原因:rank等于中位数的这个值在生成的数列中不一定存在,且这个值一定大于要找的中位数
//所以这里规定中位数是向下取整
{
right=mid-1;
ans=mid;
}
else
left=mid+1;
}
return ans;
}
该算法经过如上的优化,时间复杂度约为O(n(logn)^2),可以满足题目的要求。在实现这道题目以前,对二分可能还是懵懵的,不知道什么才是二分答案;质疑怎么可能不知道目标数组是什么,就求出中位数。经过几遍的反复实现代码和研读,对二分答案有如下的总结:
二分答案是整数二分的一个高阶变种。
二分答案就是在答案可能存在的区间内,以转换生成的单调性一致的参数,不断二分最终查找出答案
具体的实现模板:
二分答案的题目中,往往目标的数组是不可求或者求出会超时的,也就是没有了整数二分的目标区间。我们要解决的是找出一个在时间复杂度限定内可求的指标来代替目标区间,从而完成二分。在找出这个指标后,进行二分的方法是将答案的上下限作为二分的上下限,对该区域内的所有元素进行查找和二分处理,最终找到答案。
#include
#include
#include
using namespace std;
int a[100001];
int search_j(int find_b,int left,int right)//找到j点(小于等于find_b的最后一个点)
{
int ans=-1;
int mid=0;
while(left<=right)
{
mid=(left+right)/2;
if(a[mid]<=find_b)
{
ans=mid;
left=mid+1;
}
else
right=mid-1;
}
return ans;
}
int search_mid(int num)
{
int left=0;
int right=a[num-1]-a[0];
int mid=0;
int ans=-1;
int mid_pos=(num*(num-1)/2+1)/2;
while(right>=left)
{
int rank=0;
mid=(left+right)/2;
for(int i=0;i<num;i++)
{
int flag=search_j(a[i]+mid,0,num-1);
if(flag!=-1)
rank+=(flag-i);
}
if(rank>=mid_pos)
{
right=mid-1;
ans=mid;
}
else
left=mid+1;
}
return ans;
}
int main()
{
int number=0;
while(scanf("%d",&number)!=EOF)
{
for(int i=0;i<number;i++)
{
scanf("%d",&a[i]);
}
sort(a,a+number);
int result=search_mid(number);
cout<<result<<endl;
}
return 0;
}