在除夕和春节刷题的我,终于是搞懂了一点点二分的东西,也是今天才知道自己是一点都不会二分。
先放题目链接:https://vjudge.net/contest/422493#problem/B
Given N numbers, X1, X2, … , XN, let us calculate the difference of every pair of numbers: ∣Xi - Xj∣ (1 ≤ i < j ≤ N). We can get C(N,2) differences through this work, and now your task is to find the median of the differences as quickly as you can!
Note in this problem, the median is defined as the (m/2)-th smallest number if m,the amount of the differences, is even. For example, you have to find the third smallest one in the case of m = 6.
Input
The input consists of several test cases.
In each test case, N will be given in the first line. Then N numbers are given, representing X1, X2, … , XN, ( Xi ≤ 1,000,000,000 3 ≤ N ≤ 1,00,000 )
Output
For each test case, output the median in a separate line.
Sample Input
4
1 3 2 4
3
1 10 2
Sample Output
1
8
题目大意:已知有n个元素的数组,求,每对元素的差(全部取绝对值)的中位数(如果元素的对数是偶数就取中间两个靠左的那个)。
看到这个题目,正常人首先应该会想到,这题和二分有关系?难道这个不是把差值的数组求出来,然后sort排个序就好了的事情?然后回头看了看数据范围3 ≤ N ≤ 1,00,000,N最大是1e5,也就是如果要两两算差值,计算的次数约为n*(n-1) / 2,同时还要对这个数组进行排序,emmm,看起来是不太可能,所以就应该想一想正解,此处参考了很多其他巨佬的代码,链接如下:https://blog.csdn.net/wentong_Xu/article/details/87025671
根据yxc大佬的思想,我们可以把一个计算性质的问题转化成一个判断是否合法的二分算法,既然我们不好直接去排序整个差值数组,但是我们已知的是差值的上限和下限,以及,还有一点,差值的中位数的特点是:有恰好一半的数字对(对一半的解释:个数为n*(n-1)/2的中央,应该是[(n-1)*n/2 + 1]/2,这个地方比较容易迷惑人,我之前是直接除以2了,但是仔细想想,如果个数是偶数,那么我这个做法是正确的,但是如果个数是奇数,那么我们需要把个数加上1再/2,因为我们需要求中间的那个数字 )是满足差值的大小是不大于这个中位数的。仔细想一想,多考虑一下相等的情况,这句话就迎刃而解了。
于是我们可以把问题变为,我从0枚举到数组中最大值减去最小值的差值,判断这次枚举的mid是不是满足上面的差值的中位数的条件,如果满足就输出答案就行了。
同时,二分是需要有单调性的,当你枚举的数字越大的时候,满足这个差值的大小不大于这个中位数的数字对数就会越多。也是因为这样,这道题才可以用二分法来解决。
接下来考虑二分的细节。依旧是根据yxc大佬的模板https://www.acwing.com/blog/content/31/
思考,check函数应该怎么写呢。
根据模板,我们需要先判断,当前check的mid是分在左边还是右边的,如果对这个问题没有感觉,那就想一想check函数的意义是什么:“判断mid的值合法与否”,这个合法,也许是相当于上文所述的差值的中位数的特点,这里再复述一遍:有恰好一半的数字对是满足差值的大小是不大于这个中位数的。
但是再次仔细思考,你会发现,这个并不能当作check的写法,因为满足这个条件的只有中位数和和中位数相等的数字。但是,我们二分的目的是为了找到这个中位数,并不是为了判断这个数字是不是中位数,只有当搜索的区间l==r,也就是区间只剩下一个元素,尽管不会进行最后一次的check(当然此时如果check是一定返回true的)我们才能在这个情况下知道这个区间中仅剩的数字是题目所求的中位数。思考清楚check函数的意义的时候,就应该想得通,为什么这check函数应该返回,满足差值的大小不大于这次枚举的mid的数字对是大于等于一半
即[((n * (n - 1) / 2) + 1) / 2](同时该条件下文简称A条件,当然如果是小于等于一半也是一样的,本文就以大于等于一半为例)。再想,如果这个check返回1,说明mid是满足A条件的,那么根据这个条件,我们知道,我们这次枚举的mid有点大(因为差值<=mid的数字对超过了一半,而差值<=中位数的数字对数恰好是一半),我们就需要把搜索的区间削减一半,变成左半边。继续思考边界条件,很容易知道,check函数返回真说明mid满足条件A,并且,最后我们求的中位数也是满足这个条件A 的,因为大于等于是包括等于的。从这里开始,我们才知道,二分模板应该选用前一个,此处粘贴一下源码,方便查看:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
然后此处顺便敲出main函数:
#include
#include
#include
#include
using namespace std;
const int MAXN = 1e6 + 10;
int a[MAXN];
int n;
int main()
{
while (~scanf("%d", &n))
{
for (int i = 0; i > 1;
if (check(mid))r = mid;
else l = mid + 1;
}
cout << r << '\n';
}
return 0;
}
然后接下来的重点又回到了check函数:
再次总结一下check函数:
1.返回条件A
return cnt >= ((n * (n - 1) / 2) + 1) / 2;//cnt是满足条件A的数字的对数。
2.统计对于每一个数字,共有多少对数字对满足差值的大小不大于这次枚举的mid
唔,好像还是有点难,怎么样去实现第二点呢,刚刚一顿分析猛如虎,我们应该知道我们需要sort原数组对吧,那么我们是不是可以根据sort之后的数组是有序的这个条件来降低我们做统计的复杂度呢?
再次回看上文,二分是具有单调性的,假如,对于某个a[i]与a[j]是满足差值的大小是不大于这次枚举的mid的话,假设,j - 1 > i,那么这个a[i]和a[j-1]是不是也满足差值的大小是不大于这次枚举的mid呀,再深入的想一想,满足这个条件和不满足这个条件是不是有一个分界点,这个分界点是不是就是第一个不满足这个条件的那个j呀,所以我们是不是可以找到这个j,然后用下标相减的方式算出对于这次枚举的mid,一共有多少个数字对满足值的大小不大于这次枚举的mid。继续转化问题,寻找这个j,现在需要介绍一个函数
按照惯例,放大佬博客:
https://blog.csdn.net/qq_40160605/article/details/80150252
当中有对upper_bound和lower_bound两个函数有较为简要的解释,复制如下:
upper_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
所以我们可以通过这个函数,将传入的num参数变为a[i]+d,此处不做过多讲解,以后我自己回来复习的时候也得思考一下,。贴上代码:
bool check(int d)
{
int cnt = 0;
for (int i = 0; i < n; ++i)
{
int t = upper_bound(a, a + n, d + a[i]) - (a);
cnt += t - 1 -i;
}
return cnt >= ((n * (n - 1) / 2) + 1) / 2;
}
然后,全题最核心的要素来了,怼二分其实是怼下标怼了两天。
先放代码具体问题都在代码内部注释。
/*
#include
#include
#include
#include
using namespace std;
const int MAXN = 1e6 + 10;
int a[MAXN];//用于存放数组
int n;//数组元素个数
bool check(int d)//判断这次枚举的mid是否合法
{
int cnt = 0;//统计对于每一个数字,共有多少数字对满足差值的大小不大于这次枚举的mid
for (int i = 1; i <= n; ++i)//这段代码的下标是从1到n
{
int t = upper_bound(a + 1, a+n+1, d + a[i]) - a;//upper_bound二分查找第一个大于num的数字,找到返回该数字的地址,减去数组地址
cnt += t - i - 1; //我们需要求出从i到t(不包括i和t)共有多少个数字,所以需要计算t - i - 1的值
}
return cnt >= ((n * (n - 1)/ 2)+1) / 2;//返回满足差值的大小不大于这次枚举的mid的数字对是大于等于一半
}
int main()
{
while (~scanf("%d",&n))//输入到文件结束
{
for (int i = 1; i <= n; ++i)//读入数组
scanf("%d", &a[i]);
sort(a+1, a+n+1);//对数组进行排序
int l = 0, r = a[n] - a[1];//枚举的区间是[0,a[n]-a[1]]
while (l < r)//二分模板
{
int mid = (l + r) >> 1;//求mid
if (check(mid))r = mid;//对mid进行check,如果check返回真,则取左半边区间
else l = mid + 1;//否则取右半边
}
cout << r << '\n';//输出结果l或者r均可
}
return 0;
}*/
#include
#include
#include
#include
using namespace std;
const int MAXN = 1e6 + 10;
int a[MAXN];//用于存放数组
int n;//数组元素个数
bool check(int d)//判断这次枚举的mid是否合法
{
int cnt = 0;//统计对于每一个数字,共有多少数字对满足差值的大小不大于这次枚举的mid
for (int i = 0; i < n; ++i)//这段代码的下标是从0到n-1
{
int t = upper_bound(a, a + n, d + a[i]) - a;//upper_bound二分查找第一个大于num的数字,找到返回该数字的地址,减去数组地址
cnt += t - i - 1; //我们需要求出从i到t(不包括i和t)共有多少个数字,所以需要计算t - i - 1的值
}
return cnt >= ((n * (n - 1) / 2) + 1) / 2;//返回满足差值的大小不大于这次枚举的mid的数字对是大于等于一半
}
int main()
{
while (~scanf("%d", &n))//输入到文件结束
{
for (int i = 0; i > 1;//求mid
if (check(mid))r = mid;//对mid进行check,如果check返回真,则取左半边区间
else l = mid + 1;//否则取右半边
}
cout << r << '\n';//输出结果l或者r均可
}
return 0;
}
举一个样例
4
1 3 2 4
那么这个数组会被排序成
下标 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | 2 | 3 | 4 |
第一趟upper_bound,传入的d(也就是mid) 是1,应该返回数字3的地址,再减去数组的第一个元素的地址,就得到了数字3的下标。那么接下来的那个cnt += t - i-1是用到了计数原理,i到t(不包括i和t)之间的数字个数是t-i-1,所以应该要写成 cnt += t- i -1;
嗯,我要结束第一篇的博客了,感谢各位催我写博客的人,我还是感觉到了写博客的好处的,这个和我教别人并无二致,那么废话少讲,期待鸽子的下一篇博客吧!
第一次写博客,markdown就没用过,效果可能很差,请各位见谅,本文内容部分参考其他博主,侵删。