二分查找 - 手写模板与自带函数
手写模板
因为临界值有左右两侧,所以二分可以分为两种类别:求最后一个不满足要求的数,求第一个满足要求的数。
模板
手写模板很多,这里推荐一种最无脑的,无需注意开闭区间的,大部分情况不用在多个模板里切换的万能模板:
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 )
应用实例
二分查找模板,包含了第一个满足要求的、最后一个不满足要求的、无解情况的判断。
(忽略“下标从 \(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;
}
查找最后一个满足要求的数的模板
int l=1,r=n,mid;
while(l<r)
{
mid=(l+r+1)>>1;
if(check(mid))l=mid;
else r=mid-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 则与之相反。