整数二分:二分的本质并不是单调性,即有单调性一定可以用二分,但没有单调性也可能可以用二分;二分的本质其实是边界。
简单来说:给定一个区间,在这区间上定义了某种性质,使得在右半边区间是满足的,在左半边区间是不满足的,如果可以找到这个性质,即将整个区间一分为二,使得一半满足,一半不满足,那么就可以采用二分来寻找这个性质的边界。
既可以二分出红色边界点,也可以二分出绿色边界点,这又是两个不同的模板
模板一(二分红色边界点):
- 中间值mid=(l+r+1)/2(思考一下为什么+1),判断中间值是否满足这个性质:if(check(mid))
- 如果满足if(true):mid一定在红色区间,答案在[mid,r],更新方式:l=mid
- 如果不满足if(false):mid一定在绿色区间,答案在[l,mid-1](因为不满足,所以答案一定不在mid上),更新方式:r=mid-1
//区间[l,r]被划分为[l,mid-1]和[mid,r]时使用;
int bsearch_1(int l,int r){
while(l<r){
int mid=(l+r+1)>>1;
if(check(mid)) l=mid; //check判断是否满足性质
else r=mid-1;
}
return l;
}
模板二(二分绿色边界点):
- 中间值mid=(l+r)/2,判断中间值是否满足这个性质:if(check(mid))
- 如果满足if(true):mid一定在绿色区间,答案在[l,mid],更新方式:r=mid
- 如果不满足if(false):mid一定在红色区间,答案在[mid+1,r](因为不满足,所以答案一定不在mid上),更新方式:l=mid+1
//区间[l,r]被划分为[l,mid]和[mid+1,r]时使用;
int bsearch_2(int l,int r){
while(l<r){
int mid=(l+r)>>1;
if(check(mid)) r=mid; //check判断是否满足性质
else l=mid+1;
}
return l;
}
如何分析二分问题:
先写一个check()函数,再思考ture 或 false 情况如何更新,如果更新方式是l=mid,则需要补上+1,如果是r=mid,则不需要补上+1
为什么要补上加一:
举个例子:因为c++里是下取整,当 l=r-1,mid=(l+r)/2下取整等于 l,模板一if(true)中区间[mid,r],更新时,l=mid=l一直不变,造成死循环;补上加一,mid=(l+r+1)/2=r,更新时l=mid=r,[r,r]就停止不会发生死循环
题目:
思路:
- 元素 k 将整个数组分为左右两边
- 二分元素 k 起始位置:性质定为q[mid]>=k,所以从k往后所有数都满足>=k ,如果满足性质,则mid在右半边,那么答案(边界)在左半边,并且答案包含mid,所以要更新为[l,mid],r=mid(不补+1),即模板二
- 二分元素 k 结束位置:性质定为q[mid]<=k,所以从k往前所有数都满足<=k ,如果满足性质,则mid在左半边,那么答案(边界)在右半边,并且答案包含mid,所以要更新为[mid,r],l=mid(补+1),即模板一
- 数组中不存在 k 元素时:因为最终二分出来的值是从左往右第一个满足>=k的数,如果 k 不存在,那么这个值一定>k,即q[l]!=k
代码:
#include<iostream>
using namespace std;
const int N=1e5+10;
int n,q;
int s[N];
int main(){
cin>>n>>q;
for(int i=0;i<n;i++)cin>>s[i];
while(q--){
int k;
cin>>k;
//二分出起始边界
int l=0,r=n-1;
while(l<r){
int mid=(l+r)>>1;
if(s[mid]>=k)r=mid;
else l=mid+1;
}
//数组中没有该元素时
if(s[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(s[mid]<=k)l=mid;
else r=mid-1;
}
cout<<l<<endl;
}
}
return 0;
}
总结:
整数二分都可以用这两个模板解决,主要思想是在一个区间内部去二分答案(边界),每次都要选择答案所在的区间进行下一步操作,保证该区间内一定有答案,当区间长度是1时,区间里的数就是答案。题目可能是无解,但是二分的模板一定是有解的。
有关算法竞赛的题目会继续更新,欢迎评论交流!