二分查找与二分答案

大家也许玩过这样一个游戏:A在心中想一个一百以内的数,B猜一个数后A来回答大了还是小了,直到B猜出这个数。那么,应怎样尽快结束这个游戏?聪明的你想到了使用一半一半地猜的方法。即先猜50再靠对方的回答来决定猜25或者75,并以此类推直到猜出这个数。而这正是二分查找的应用。

一、二分查找

二分查找是一种非常高效的搜索方法,主要原理是每次搜索可以抛弃一半的值来缩小范围,一般用于对普通搜索方法的优化。(时间复杂度为 (long2n) )。但是,使用此方法有一个前提,有序性。也就是说,在进行查找之前,要对数组进行排序。

基本思路

(一)先对n个数进行排序。

(二)再有顺序的n个数中查找x。

①先让x与区间中间值mid比较,等于就结束

②若x<a[mid],则说明x应在a[0]到a[mid]之间,调整区间右端点

③若x>a[mid],则说明x应在a[mid]到a[n-1]之间,调整区间左端点

循环重复进行,每次查找都拿新的区间中间值与x做比较

代码实现

cpp 复制代码
int l;                            //区间左端点
int r;                            //区间右端点
while(l<=r) {                     //循环进行查找
    int mid=(l+r)/2;              //求区间中间值
    if (mid==答案) {              //将中间值与答案进行比较
        求出结果了,可进行下一步操作
    }
    else if (mid<答案) l=mid+1;   //mid小于答案,调整左端点
    else r=mid-1;                 //mid大于答案,调整右端点
}

题目详解

A-B 数对

题面

P1102 A-B 数对 - 洛谷

思路

本题最重要的思路即将题面中的A-B=C转换为A+C=B,通过循环来确定和更换A的值,并将A与C相加得到希望能找到的B的值。再通过二分来确定是否存在与个数。

其中值得注意的是,在n个正整数中,可能会有重复,且不同位置的数字一样的数对算不同的数对。所以应该找的是某个值所有的个数。

那么,怎么求某个值所有的个数呢?

在将这n个数排序后,值相等的数会形成一个区间。所以,只需要找到该区间的左端点与右端点取其之差就可以了。

寻找左端点(即找大于等于的数,其输出的位置是同类型中最靠左的):

其与上述代码大体相似,但mid的判断与while循环条件等不太一样。

cpp 复制代码
int l;                            //区间左端点
int r;                            //区间右端点
while(l<r) {                      //区间内至少有一个元素时循环
    int mid=(l+r)/2;              //求区间中间值
    if (mid<答案) l=mid+1;        //mid小于答案,调整左端点
    else if (mid>=答案) r=mid;    //mid大于等于答案,调整右端点(可以只写一个else)
}

将循环条件改为l<r可以避免死循环,例如当l=3,r=4时,mid=3。若此时mid>=答案,则r=mid=3,若此时循环条件为l<=r则会死循环,但将其改为l<r就可以避免。

将r=mid-1改为r=mid可以保留mid,因为在求大于等于的数时,mid就算大于答案也可能是我们要求的值。

寻找右端点(即找小于等于的数,其输出的位置是同类型中最靠右的):

此与上文找大于等于的数基本相同,不再赘述。

cpp 复制代码
int l;                            //区间左端点
int r;                            //区间右端点
while(l<r) {                      //区间内至少有一个元素时循环
    int mid=(l+r+1)/2;            //求区间中间值,+1目的为向上取整
    if (mid<=答案) l=mid;         //mid小于等于答案,调整左端点
    else if (mid>答案) r=mid-1;   //mid大于答案,调整右端点
}

向上取整的目的是避免死循环。比如说当l与r相邻时,若向下取整(mid=(l+r)/2),那么mid就等于l。之后若mid<=答案成立,则l=mid=l,l的值没有变,区间大小也没有变,陷入了死循环。

代码
cpp 复制代码
#include <bits/stdc++.h>            //万能头文件
using namespace std;
long long n,c,a[200005],ans,sum;    //要求处理的数字个数n,数对结果c,要求处理的数字a数组,每一次目标要求的b的值ans,满足a-b=c的数对个数sum
int main() {
	cin>>n>>c;                      //输入n与c 
	for (int i=1;i<=n;i++) {        //进行n次循环
		cin>>a[i];                  //输入a数组 
	}
	sort(a+1,a+n+1);                //对a数组进行排序
	long long l,r,l1,r1,mid;        //定义求左端点和右端点会用到的变量
	for (int i=1;i<=n;i++) {        //进行n次循环,n个数轮流当A
		ans=c+a[i];                 //将已知的A与C相加得到目标要求的ans的值
		l=1,r=n;                    //将求左端点用的变量初始化
		l1=1,r1=n;                  //将求右端点用的变量初始化
		while (l<r) {               //求左端点
			mid=(l+r)/2;            //求区间中间值
			if (a[mid]<ans) {       //当中间值小于目标要求的ans的值时
				l=mid+1;            //调整l
			}
			else {                  //当中间值大于等于目标要求的ans的值时
				r=mid;              //调整r
			}
		}
		while (l1<r1) {             //求右端点
			mid=(l1+r1+1)/2;        //求区间中间值,向上取整
			if (a[mid]>ans) {       //当中间值大于目标要求的ans的值时
				r1=mid-1;           //调整r1
			}
			else {                  //当中间值小于等于目标要求的ans的值时
				l1=mid;             //调整l1
			}
		}
		if (a[l]==ans&&a[l1]==ans) {//因为a[l]与a[l1]有可能不等于目标要求的ans的值,所以需要再判断一下
			sum+=abs(l1-l+1);       //计算与ans相等的数的个数
		}
	}
	cout<<sum;                      //输出总数
	return 0;
}

查找

题面

P2249 【深基13.例1】查找 - 洛谷

思路

本题相较于上文中所述的A-B数对来说更简单,仅求出左端点(在序列中第一次出现的位置)就可以了。求出左端点的方法前文已叙述,这里不多赘述。

且本题输入的序列是从大到小的有序数列,符合使用二分查找的条件。

代码
cpp 复制代码
#include <bits/stdc++.h>        //万能头文件
using namespace std;
long long n,m,a[1000005],b;     //序列数字个数n,询问次数m,序列数字a数组,m次询问中所询问的数字b
int main() {
	cin>>n>>m;                  //输入序列数字个数与询问次数
	for (int i=1;i<=n;i++) {    //进行n次循环
		cin>>a[i];              //输入序列
	}
	long long l,r,mid;          //定义寻找左端点所用的变量
	for (int i=1;i<=m;i++) {    //进行m次循环
		cin>>b;                 //输入所询问数字
		l=1,r=n;                //初始化变量
		while (l<r) {           //用while循环进行查找
			mid=(l+r)/2;        //计算区间中间值
			if (a[mid]<b) {     //当中间值小于所询问数字时
				l=mid+1;        //调整l
			}
			else {              //当中间值大于等于所询问数字时
				r=mid;          //调整r
			}
		}                       //使用此方法求的是大于等于的值,有不相等的风险,因此在查找后仍需判断一下
		if (a[l]==b) {          //当查找出来的数与所询问数字相等时
			cout<<l<<" ";       //输出答案位置
		}
		else {                  //当查找出来的数与所询问数字不相等时
			cout<<"-1"<<" ";    //输出-1
		}
	}
	return 0;
}

二、二分答案

当答案属于一个区间,且区间很大(容易超时),并且对题目中的某个量有单调性(最重要)时,就会使用二分答案。

与二分查找的异同

二分查找是在一个已知的有序数对上进行查找。二分答案则是在最优答案所在区间进行查找,直到找到它。

它们进行二分的写法差不多,仅仅只是if语句判断条件的不同。在二分答案里,我们需要依照题意写一个检查函数(check函数,可缩写成c函数),并将中间值带入来判断是舍弃左区间还是右区间。

题目详解

跳石头

题面

https://www.luogu.com.cn/problem/P2678

思路

通过读题可知,本题有答案区间,可使用二分答案。本体题面中"使得选手们在比赛过程中的最短跳跃距离尽可能长"告诉了我们本题是求最小值的最大值,所以说是套上文中求小于等于的公式。

代码
cpp 复制代码
#include <bits/stdc++.h>            //万能头文件
using namespace std;
long long l,m,n,d[50005];           //起点到终点的距离l,组委会至多移走的岩石数m,起点和终点之间的岩石数n,第i块岩石与起点的距离d数组     
bool c(long long x) {               //检查函数,布尔值类型
	int s=0,z=0;                    //移走的岩石数s,上一块石头位置z
	for (int i=1;i<=n+1;i++) {      //进行(n+1)次循环,因为最后一块石头也要跳
		if (d[i]-z<x) {             //如果这一次跳跃距离小于最短跳跃距离
			s++;                    //说明需要移走当前石头
			if (s>m) {              //移走的石头数量超过了至多移走的岩石数
				return 1;           //此x不是答案
			}
		}
		else {                      //如果这一次跳跃距离大于等于最短跳跃距离
			z=d[i];                 //跳一下,记录位置
		}
	} 
	return 0;                       //此x可能是答案
}
int main() {
	cin>>l>>n>>m;                   //输入起点到终点的距离,起点和终点之间的岩石数和至多移走的岩石数
	for (int i=1;i<=n;i++) {        //进行n次循环
		cin>>d[i];                  //输入第i块岩石与起点的距离
	}
	d[n+1]=l;                       //最后一块石头也要跳,记录一下距离
	long long le=1,r=l,mid;         //定义变量
	while (le<r) {                  //二分答案分的是最短跳跃距离
		mid=(le+r+1)/2;             //计算中间值,向上取整防死循环
		if (c(mid)) {               //进行判断,发现此mid不是答案
			r=mid-1;                //说明mid大了,调整r
		}
		else {                      //进行判断,发现此mid可能是答案
			le=mid;                 //保留一下,继续二分
		}
	}
	cout<<le;                       //输出答案
	return 0;
}

砍树

题面

P1873 [COCI 2011/2012 #5] EKO / 砍树 - 洛谷

思路

本题与上题较相似,本题二分的是锯片的最高高度。从题面中"如果再升高 1 米,他将得不到 M 米木材。"可知本题是求最小值的最大值,所以说是套上文中求小于等于的公式。

代码
cpp 复制代码
#include <bits/stdc++.h>            //万能头文件
using namespace std;
long long n,m,a[1000005],l,r,mid;   //树木的数量n,需要的木材总长度m,每棵树的高度a数组,后面二分锯片的最高高度所要用到的l,r,mid
bool c (long long x) {              //检查函数
	long long s=0;                  //得到的木材总长度
	for (int i=1;i<=n;i++) {        //砍了n棵树,所以进行n次循环
		if (a[i]>x) {               //如果这棵树会被锯片砍到,即它的高度高于锯片高度
			s+=a[i]-x;              //将砍下的木材加入得到的木材总长度
		}
	}   
	if (s<m) {                      //如果根本满足不了目标
		return 0;                   //此为错误答案
	}
	return 1;                       //此可能为最优答案          
}
int main() {
	cin>>n>>m;                      //输入树木的数量与需要的木材总长度
	for (int i=1;i<=n;i++) {        //进行n次循环
		cin>>a[i];                  //输入每棵树的高度
		if (r<a[i]) {               //区间右端点的值是树的高度的最大值
			r=a[i];                                
		}                                                        
	}                                                    
	sort(a+1,a+n+1);                //将树按高度从小到大进行排序
	while (l<r) {                   //进行二分答案
		mid=(l+r+1)/2;              //计算区间中间值
		if (!c(mid)) {              //如果连目标都满足不了
			r=mid-1;                //说明锯片高了,调整r
		}
		else {                      //如果此可能为最优答案   
			l=mid;                  //保留mid并调整l
		}
	}
	cout<<l;                        //输出答案
	return 0;
}

三、总结

二分算法使用起来较简单,不是很灵活,记住公式在题目中灵活应变即可。

P.S.在读题时一定要读明白,知道是求最小值的最大值(用找小于等于的数的公式)还是最大值的最小值(找大于等于的数的公式)。

在最后,给大家分享个口决吧。

二分最怕死循环,mid计算是关键。

l=mid必须上取整,否则相邻就卡死。

r=mid必须下取整,对称规则要记全。

检查函数写清晰,贪心模拟莫跑偏。

相关推荐
得一录1 小时前
Python 算法高级篇:布谷鸟哈希算法与分布式哈希表
python·算法·aigc·哈希算法
郝学胜-神的一滴1 小时前
Effective Modern C++ 条款37:使std::thread在所有路径最后都不可结合
开发语言·c++·程序人生·多线程·并发·std
AC赳赳老秦1 小时前
2026 智能制造趋势:DeepSeek 助力“黑灯”工厂运营,实现生产流程自动化
网络·数据结构·算法·安全·web安全·prometheus·deepseek
流云鹤1 小时前
2026牛客寒假算法基础集训营6(K H G B A)
算法
程序员酥皮蛋1 小时前
hot 100 第三十题 30. 两两交换链表中的节点
数据结构·算法·leetcode·链表
寻寻觅觅☆2 小时前
东华OJ-基础题-131-8皇后·改(C++)
c++·算法·深度优先
程序员徐师兄2 小时前
基于 Python 深度学习的电影评论情感分析算法
python·深度学习·算法·电影情感分析算法·评论情感分析
ShineWinsu2 小时前
对于C++中list的详细介绍
开发语言·数据结构·c++·算法·面试·stl·list
_OP_CHEN2 小时前
【算法提高篇】(三)线段树之维护更多的信息:从基础到进阶的灵活运用
算法·蓝桥杯·线段树·c/c++·区间查询·acm/icpc·信息维护