二分查找 - 手写模板与自带函数

手写模板

因为临界值有左右两侧,所以二分可以分为两种类别:求最后一个不满足要求的数,求第一个满足要求的数。

模板

手写模板很多,这里推荐一种最无脑的,无需注意开闭区间的,大部分情况不用在多个模板里切换的万能模板:

int l=0,r=n,mid;    //左闭右闭区间
while(l<r)    //直到左闭右闭区间只有一个数
{
	mid=(l+r)>>1;    //位运算加速
	if(条件)r=mid;    //如果满足条件,就说明 r 可能是答案,但 r 后面的不可能是答案
	else l=mid+1;    //否则就说明 l 和 l 左边的都不可能是答案
}
cout<<l;    //这里输出 l 和 r 都是一样的

这个求的是第一个满足“条件”的数。区间为左开右开,答案输出 \(l\)\(r\) 都可以。

对于求第一个满足要求的数,直接套用该模板。

而对于求最后一个不满足要求的数,套用这个模板后答案就是 \(l-1\) (写成 \(r-1\) 也可以)。因为最后一个不满足要求的数的下一个数,就是满足要求的第一个数

唯一要动脑的地方就是写“条件”。一定要想清楚等于的情况到底是缩小左边还是右边。

无注释版:

int l=,r=,mid;
while(l<r)
{
	mid=(l+r)>>1;
	if()r=mid;
	else l=mid+1;
}
cout<<l;

坑点

这种写法也有缺陷,就是二分如果找不到方案,会自动指向最后一个元素,即 \(r\) 的初值。

此时如果是要 \(-1\) 的“寻找最后一个不满足要求的数”,则可能出现把有解情况判断成无解的错误。因为我们原本要找的可能恰好就在最后一个,再后面就没有了。

为了解决这个问题,我们可以在 \(n+1\) 的位置设置一个无论查找什么都是 true 的极大值,并且把 \(r\) 设置为 \(n+1\) 来容纳多出的一个“虚拟节点”,来确保一定能找到答案。如果最后答案不合法,则说明无解。不能判断答案是否指向最后我们新加的元素,因为无论是原本有解在最后一个元素,还是本来就无解,都会被判断成无解。所以直接判断答案合不合法就好了。

构造那个极大值,既可以通过手动构造极大值的方式,也可以在 check 里加一个特判。这里推荐在 check 里直接加 mid==n+1 的特判。( 不是 r+1 )

应用实例

二分查找模板,包含了第一个满足要求的、最后一个不满足要求的、无解情况的判断。

image

(忽略“下标从 \(0\) 开始”的条件,这里我们把下标规定为 \(1\) 开始。)

#include <bits/stdc++.h>
using namespace std;
int n,q,a[100005],x;
int find_start(int p)
{
	int l=1,r=n+1,mid;
	while(l<r)
	{
		mid=(l+r)>>1;
		if(a[mid]>=p||mid==n+1)r=mid;    //特判新加的点始终为 true,这里注意是 n+1 不是 r+1
		else l=mid+1;
	}
	if(a[l]!=p)return -1;    //判断是否合法,而不是判断是否指向新加的点
	return l;
}
int find_end(int p)
{
	int l=1,r=n+1,mid;
	while(l<r)
	{
		mid=(l+r)>>1;
		if(a[mid]>p||mid==n+1)r=mid;    //特判新加的点始终为 true,这里注意是 n+1 不是 r+1
		else l=mid+1;
	}
	l--;    //变成求最后一个不满足的
	if(a[l]!=p)return -1;    //判断是否合法,而不是判断是否指向新加的点
	return l;
}
int main()
{
	cin>>n>>q;
	for(int i=1;i<=n;i++)cin>>a[i];
	while(q--)
	{
		cin>>x;
		cout<<find_start(x)<<' '<<find_end(x)<<endl;
	}
	return 0;
}

find_end()函数也可以这么写:

int find_end(int p)
{
	int l=1,r=n,mid;
	while(l<r)
	{
		mid=(l+r+1)>>1;
		if(a[mid]<=p)l=mid;
		else r=mid-1;
	}
	if(a[l]!=p)return -1;
	return l;
}

查找最后一个不满足要求的数的模板

自带函数

lower_bound()

基础用法:返回下标:lower_bound(a+1,a+n+1,val,cmp)-a

不加 cmp 时:返回第一个大于等于 \(val\) 的下标。

加 cmp 时 :返回第一个 false 时的下标。

cmp 函数写法:bool cmp(const int &e,const int &val){}

#include <bits/stdc++.h>
using namespace std;
int n,a[100005];
bool cmp(const int &e,const int &val)
{
	return e>=val;
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	int t;
	cin>>t;
	while(t--)
	{
		int x;
		cin>>x;
		cout<<lower_bound(a+1,a+n+1,x,cmp)-a<<endl;
	}
	return 0;
}

这个代码写的是:一个从大到小的有序数组中,第一个小于 \(x\) 的数的下标是多少。

upper_bound()

基础用法:返回下标:upper_bound(a+1,a+n+1,val,cmp)-a

不加 cmp 时:返回第一个大于 \(val\) 的下标。

加 cmp 时 :返回第一个 true 时的下标。

cmp 函数写法:bool cmp(const int &val,const int &e){}

#include <bits/stdc++.h>
using namespace std;
int n,a[100005];
bool cmp(const int &val,const int &e)
{
	return e<val;
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	int t;
	cin>>t;
	while(t--)
	{
		int x;
		cin>>x;
		cout<<upper_bound(a+1,a+n+1,x,cmp)-a<<endl;
	}
	return 0;
}

这个代码写的是:一个从大到小的有序数组中,第一个小于 \(x\) 的数的下标是多少。

总结

在 upper_bound 与 lower_bound 中,更推荐使用 upper_bound ,因为他返回的是第一个 true 的元素,写 cmp 更加容易且直观。

唯一要注意的是,upper_bound 的 cmp 函数 \(val\) 在前,\(e\) 在后。( \(val\) 是要查找的元素,也就是传的第三个参数。),lower_bound 则与之相反。

posted @ 2024-07-21 23:57  KS_Fszha  阅读(6)  评论(0编辑  收藏  举报