二分查找的思想:
①确定一个区间,使得我们要找的目标值一定在这个区间内出现
②找一个性质,满足以下两点(整数二分和实数二分通用):
- (1)性质具有 二段性(100%一定成立)
-
- 二段性含义:在一段区间中,前面一段连续的部分满足这个性质,后面一段连续的部分不满足这个性质,两部分无缝衔接(满足和不满足是相对的),就如下图:
- 二段性含义:在一段区间中,前面一段连续的部分满足这个性质,后面一段连续的部分不满足这个性质,两部分无缝衔接(满足和不满足是相对的),就如下图:
- (2)二分的答案 是 二段性的分界点。以整数二分为例子,有两种情况:“红色区间的右端点” 以及 “绿色区间的起点”,如下图:
对于整数二分,二分答案有两种情况,对于不同的情况我们对应有着不同的做法。
接下来我们看看对于这两种情况如何进行整数二分。
第一类:
二分的答案ans
是 红色区间的右端点
假设当前区间范围是[L, R]
,并确保答案一定在当前区间内,设当前的中点为mid
,
对于第一类我们可以将整个区间分成两段:[L, mid-1]
、[mid, R]
- if(
mid
是红色) 说明ans
必然在[mid, R]
中,有可能和mid
重合(因为mid
和ans
都是红色的) - else if(
mid
是绿色) 说明ans
必然在[L, mid-1]
中
上述对应到代码上,整数二分模板一如下:
while(l<r)
{
int mid = (l+r+1)/2;//如果下方是l=mid,则应当补上+1
if(mid是红色) l=mid;
else r=mid-1;
}
第二类:二分的答案ans
是 绿色区间的左端点
对于第二类我们可以将整个区间分成两段:[L, mid]
、[mid+1, R]
- if(
mid
是绿色) 说明ans
必然在[L, mid]
中,有可能和mid
重合(因为mid
和ans
都是绿色的) - else if(
mid
是红色) 说明ans
必然在[mid+1, R]
中(ans
在mid
严格右边,不可为mid
)
上述对应到代码上,整数二分模板二如下:
while(l<r)
{
int mid = (l+r)/2;//如果下方是r=mid,不要+1
if(mid是绿色) r=mid;
else l=mid+1;
}
总结:
例题:AcWing 789. 数的范围
题意:
给定一个已经排好序的,且长度为n
的升序数组,和q
个询问,对于每个查询返回元素k
的起始位置和终止位置
思路:
根据上面的总结,
第一步,我们先找到一个区间,使得答案一定在这个区间中,我们的区间确定为[0, n-1]
。
第二步,找一个判断条件,使得该条件具有二段性,并且答案一定是该二段性的分界点,不过这里的“答案”并不一定是这道题要求的答案,我们需要找一个目标值(边界),这个目标值不一定是这道题所要求的值。
不妨先找左端点,想一想左端点具有什么样的性质,即 用什么样的性质 使 左端点成为二段性的一个分界点。假设我们当前要找的值是x
,那么左端点相当于在整个数组中大于等于x的第一个位置。
因此,二段性可以这样设置,判断条件为:q[mid]>=x
(判断每个位置的值是否大于等于x),如果x
存在的话,那么 最终要二分的答案x
位置就一定位于满足 q[mid]>=x
的第一个位置
如上图,红色区间所有数都是不满足>=x的,绿色区间所有数都是满足>=x的,q[mid]>=x
这个条件可以将答案x
变成进行二分的一个分界点
找到左端点后,L==R
,我们还要判断是否有解,即判断是否有q[L]==q[R]
,如果不成立,则说明无解,如成立,则说明L
、R
是x
的左端点
找左端点的过程运用上面总结的整数二分模板二(左边红色部分不满足>=x
,右边绿色部分满足>=x
,寻找 绿色区间的左端点)
代码如下:
int l = 0, r = n-1;
while(l<r)
{
int mid = l + r >> 1;
if(a[mid]>=k) r = mid;//相当于:if(mid是绿色) 说明ans必然在[L, mid]中(r=mid),有可能和mid重合(因为 mid 和 ans 都是绿色的)
else l = mid + 1;
}
//如果a[l]==x,则二分出来的 l or r 即为左端点的性质
之后找一下右端点(下图三角形所指位置),我们定一段区间:[上面找到的左边界, n-1],
我们定一个性质:q[mid]<=x
,三角形及其左边的元素都是<=x
的,三角形严格右边都是>x
的,因此这个判断条件是具有二段性的,且答案(右端点)一定是该二段性的分界点。
因此寻找右端点的过程就对应的是上面总结的整数二分模板一(左边红色部分(对应上图三角形及其左边) 满足<=x
,右边绿色部分(对应上图三角形严格右边) 满足>=x
,寻找 红色区间的右端点)
代码如下:
r = n - 1;
while(l<r)
{
int mid = l + r + 1 >> 1;
if(a[mid]<=k) l = mid;
else r = mid - 1;
}
时间复杂度:
O(nlogn)
总代码如下:
//二分查找左右边界位置
#include<iostream>
using namespace std;
const int N = 1e5+10;
int n,q,k;
int arr[N];
bool check1(int mid)//寻找左边界判断函数
{
if(arr[mid]>=k) return true;
return false;
}
bool check2(int mid)//寻找右边界判断函数
{
if(arr[mid]<=k) return true;
return false;
}
int main()
{
cin>>n>>q;
for(int i=0;i<n;i++)
scanf("%d", &arr[i]);
while(q--)
{
cin>>k;
int l=0,r=n-1;
while(l<r)
{
int mid=l+r>>1;
if(check1(mid)) r=mid;
else l=mid+1;
}
if(arr[l]!=k) cout<<"-1 -1"<<endl;
else
{
cout<<l<<' ';
int l=0,r=n-1;
while(l<r)
{
int mid=l+r+1>>1;
if(check2(mid)) l=mid;//当更新方式为l=mid时,上方mid要加1防止出现死循环
else r=mid-1;
}
cout<<l<<endl;
}
}
return 0;
}