目录
一、前言
晚上好...(哈欠
今天我们来讲什么呢?唔...我们来讲一个二分模板吧;
如果您使用的是C++,您可以使用lower_bound()或者up_bound()来实现数组里的二分查找,但它仍然具有局限性 ,比如说它并非精确查找,它查找的是第一个大于或者等于目标元素x的位置;
我们将来看一个模板,小夏会附上注释帮助您理解,让我们开始吧;
二、二分查找模板
2.1、模板:
bool check(int x){
}//检查x是否满足某种性质;
//区间[l,r]将被划分为[l,mid]和[mid+1, r ]时使用;;
int bsearch_1(int l, int r);
{
while ( l < r )
{
int mid = l + r >> 1;
/*等价与mid = (r + l ) / 2;
定义中点mid,向下取整;比如 l = 0, r = 5 ;
mid = (l + r ) / 2 ; mid = 2;
写对mid 的取值很重要;
*/
if(check(mid)) r = mid ; //当mid 满足某种性质时,更新区间右端r为mid;
else l = mid + 1;
/*当mid 不满足该性质时,更新区间左端l为mid + 1;
值得注意的是,r = mid 时,l 直接取它的补集开头 mid + 1:
这个是不用考虑的,您只要想明白,mid 满足某种性质的时候;
您应该怎么更新区间就可以了,else部分对应的一定是它的补集
*/
}
return l;
}
bool check(int x){
} //检查某个数是否具有某种性质;
int b_search_up(int l , int r){
while( l < r ){
int mid = ( r + l + 1) / 2;//向上取整;
if(check(mid)) l = mid ; //如果这个数字具有该种性质,将区间更新为[mid , r]
else r = mid - 1;
}
return l;
} //区间被划分为[ l , mid - 1] 和 [ mid , r ]时使用;
这两个模板将被用于整数查找;
必须强调的是,该模板在使用前必须确保您要查找的序列是有序的,否则很容易出错;
其次尽可能保证您要查找的元素在这个区间里面;
最重要的有两点:
1、归纳出 x 应该满足的性质:
比方说 x 是 正/负数,是奇/偶数,是素数,
又比方说, x 及其之后的数都比目标值大/小;
这个将由具体的问题具体分析;
2、当x满足该性质时该如何更新区间:
比方说我们在0-10内查找3;
我们先把区间分为左区间[0,5]和右区间[6,10]两个部分;
我们不难发现,右区间的所有数都比我们要找的数3要大;
这说明我们要找的数肯定不在右区间,所以我们要把右边的部分去掉;
这便是一种性质;
我们取5作为中间值,5满足了比它大的性质,所以我们要把右边去掉;
问题更新为,在[0,5]区间查找 3 ;
1、[0,5]将被划分为[0,2],[3,5]两个区间;
2、mid 取为 2,即mid = 2;
3、mid 满足 m < 3 的性质,因此我们要把左边的部分去掉;
这个问题,被更新为在 [3,5] 区间查找 3;
我们不断地重复这个过程,您会发现区间越来越小,直至逼近我们的目标值3;
如果查找不到元素3的话,会返还第一个一个大于3的数字的位置;
以下为实现代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int a[N];
int b_search_first(int a[],int l,int r,int x){
while( l < r ){
int mid = ( l + r ) >> 1;//该写法为向下取整;
if(a[mid] < x ) l = mid + 1;
else r = mid;
}
return l;
}
int main(){
int n;
cin>>n;
for(int i = 0 ; i < n ; i ++ )cin>>a[i];
sort(a,a+n);
int x;
cin>>x;
cout<<b_search_first(a,0,n-1,x);
}
接下来我们将学习另一个部分:
——在一个有序的,或者类有序的数组里面查找指定元素,返回它最后一次出现的位置;
要怎么思考这个问题呢?
首先,我们先不管三七二十一,界定 mid = (l + r ) / 2; 要不要加一我们之后再判断;
之后我们来观察 mid 指向的数字 是否满足某种性质;
对于一个已经排好顺序的数组a[6]={ 1 1 2 2 3 3 }
我们不妨以查找 数字3 最后一次出现的位置为例子;
令 l = 0 , r = 6 - 1 ;
mid = ( 0 + 5 ) / 2;
mid = 2 ;
a[mid] = 2;
我们来观察 2 ,2 比 3 小,所以我们的答案必然不在 区间 [0,2]上;
不管三七二十一,我们先更新区间 l = mid ,要不要更新为l = mid + 1 我们之后再考虑;
此时 l = 2 ,r = 5;
mid = 3 ;
a[mid] = 2 ; a[mid] < 3 更新区间为 [3,5];
重复上述步骤之后,观察a[mid] 跟 3 的关系 ;
如果 a[mid] > 3 ,那么更新 r = mid;要不要更新为 r = mid - 1 我们之后再考虑;
如果 a[mid] < 3 ,那么更新 l = mid;要不要更新为l = mid + 1 我们之后再考虑;
那么最要命的问题是,a[mid] = 3 时,您要如何抉择?
打个比方,对于这个数列
3 3 3 3;
l = 0, r = 3;
我们想要查找的是3最后一次出现的位置对吗(笑);
那么这就跟您如何取 mid 有关了;
如果您采用的是向上取整,那将是这个样子:
3 3 / 3 3 ,小夏为您划分了区间,您不可能会取前面那部分,所以您会更新区间为
l = mid ;
新的区间:
3 / 3 ; 后面是mid所指;
l = mid ;
此时 l = r ,循环结束;
(笑)小夏为您写好了代码,也请自己动手试试看吧!
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int a[N];
int b_search_last(int a[],int l,int r,int x){
while( l < r ){
int mid = l + r + 1 >> 1;
if(a[mid] > x ) r = mid - 1;
else l = mid;
}
return l;
}
int main(){
int n;
cin>>n;
for(int i = 0 ; i < n ; i++)cin>>a[i];
int x;
cin>>x;
cout<<b_search_last(a,0,n-1,x);
}
三、例题
3.1、数的范围:
这是一道小夏很喜欢的例题,小夏在学二分的时候可是被它折磨得死去活来(笑)
那么轮到您坐牢了吗(笑)还是您要潇洒地一遍AC,淡淡地说
“就这?”
题目来自ACWING网站;
给定一个按照升序排列的长度为 nn 的整数数组,以及 qq 个查询。
对于每个查询,返回一个元素 kk 的起始位置和终止位置(位置从 00 开始计数)。
如果数组中不存在该元素,则返回 -1 -1
。
输入格式
第一行包含整数 nn 和 qq,表示数组长度和询问个数。
第二行包含 nn 个整数(均在 1∼100001∼10000 范围内),表示完整数组。
接下来 qq 行,每行包含一个整数 kk,表示一个询问元素。
输出格式
共 qq 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1
。
数据范围
1≤n≤1000001≤n≤100000
1≤q≤100001≤q≤10000
1≤k≤100001≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
3.2、题解:
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int a[N];
int n,m;
int main(){
scanf("%d%d",&n,&m);
for(int i = 0 ; i < n ; i ++ )cin>>a[i];
while(m--){
int x ;
cin>>x;
int l = 0,r = n - 1 ;
while(l < r){
int mid = r + l >> 1;
if(a[mid] >= x ) r = mid ;
else l = mid + 1;
}
if(a[l]!= x) cout<<"-1";
else cout<<l;
cout<<" ";
l = 0 ,r = n - 1;
while(l < r){
int mid = r + l + 1 >> 1;
if(a[mid] > x) r = mid - 1;
else l = mid;
}
if(a[l]!= x) cout<<"-1";
else cout<<l;
cout<<endl;
}
}
就是把两个模板灵活运用好久可以了(笑);
值得注意的是:当查找不到相应元素的时候 a[l] 的值是一个比 3要大的值,您可以用它来判断是否输出‘-1’;
四、总结:
4.1、小tips
1、只有两种区间划分方式——
1.1、[l , mid] 和 [ mid + 1, r];
1.2、[l , mid - 1] 和 [ mid , r];
2、如果您采用1.1的划分方式,那么请注意,您的mid = ( l + r ) / 2,也就是所谓的向下取整;
如果您采用1.2的划分方式,那么请注意,您的mid = ( l + r + 1) / 2,也就是所谓的向上取整;
3、请您先写一个check(),
再根据check()的true 或者 false 来判断怎么划分区间,
最后根据您的划分方式确定mid如何取整;
4、二分的本质不是单调性,二分的本质是边界问题(笑)
要如何把握边界也是小夏一直在探索的课题(笑),无论是技术还是人生,它对于小夏来说都是很重要的课题;
五、后记
在新生选拔赛过后好久没有跟您见面了呢,不知道您最近还顺利吗?
小夏在准备期末考试,但也有为您认真准备一些知识呢;
希望您一切顺利,等小夏放寒假了,或许会有更多时间跟您交流吧(笑)。
唔...特别鸣谢ACWING网站提供的题目;
以及yxc先生提供的二分模板,这是小夏看到的最好的二分模板;
还有屏幕前的您,也感谢您来阅读我的博文,我们下周再见;
夏日弥死傲娇
2021.12.18;