二分查找及模板——试解《数的范围》

目录

一、前言

二、二分查找模板

        2.1、模板:

三、例题

        3.1、数的范围:

四、总结:

        4.1、小tips

 五、后记


一、前言

        晚上好...(哈欠

        今天我们来讲什么呢?唔...我们来讲一个二分模板吧;

        如果您使用的是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,淡淡地说

        “就这?”

        789. 数的范围 - AcWing题库

        题目来自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;

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值