您的位置:首页 > 其它

基础算法 - 二分查找 / 二分答案入门

2018-01-17 21:26 465 查看
没错!你没有看错!

我来写那个让我讨厌至极的二分了!

二分真的很迷,有的时候必须用二分的地方看不出要用二分,然后就一片TLE

抱怨到这里,我们来讲讲二分的原理。

二分二分,顾名思义,就是将查找的区间分成两半,找中间的部分,然后判断查找左半边还是右半边。

很显然,二分有一个非常重要的条件:查找元素必须有序

不然就难以判断它到底往左找还是往右找

同时,要注意二分查找和二分答案在原理上相同,但二分查找用于查找元素,而二分答案更像是枚举算法的优化,在做题时不要搞错用哪一种。

那么,二分到底优秀在哪呢?

我们知道,评价一个算法,我们可以从时间复杂度空间复杂度来判断

二分查找的空间复杂度:Θ(n),和一般算法相同

二分查找的时间复杂度:Θ(log22n),从这里就可以看出二分查找优秀了很多。

比如我们有105个元素,一般的方法在最坏情况下应查找105次,如果是二分的话,只需要log2210=7次即可。

所以二分查找确实优秀了非常多,也是不管在什么级别的比赛中都是非常重要的一种算法。

接下来我们来讲讲二分查找怎么写。

EG 1 二分查找

输入一组数据(个数为n)和一个数m,试在这组数据中找出有没有m

做二分查找题,在题目没有明确表示的情况下,我们最好先进行预处理

即给数组排序

这里,我果断上阵了已经喂了5圣杯的STL!

那么代码就很简单了

sort(a+1,a+n+1);


接下来开始二分查找

首先我们要确定查找的边界,即这个数可能在第几到第几的范围内出现。

很显然,这题我们并不能直接圈定一个范围,那么我们就无脑直接将范围设成整个数组

我们以 l 表示左边界,r 表示右边界



l=1,r=n;


显然,左右边界在查找过程中会发生改变,所以接下来有一个问题,见下文。

预处理结束,我们就可以开始查找了。

我们之前提到了,分左右之后找中间的部分,也就是最中间的那个。

查找肯定不能一次就结束啊,所以我们要用循环。

设置变量mid为当前查找的位置。

这边有两种写法:

e774
1. FOR
for(l=1,r=n;l<=r;)
{
mid=l+r >> 1;
}
这种写法用得较少,我个人一般也不这么写

2. WHILE
l=1,r=n;
while(l<=r)
{
mid=l+r >> 1;
}


这里的 >> 1代表二进制右移一位,即 /21,占用时间较少。

注:位运算的优先级低于加减乘除模

接下来我们要进一步判断是向左找还是向右找。

设置变量flag代表有没有找到,初始为false。

if(a[mid]==m)//找到了就弹出循环
{
flag=true;
break;
}
if(a[mid]>m) r=mid-1;
else l=mid+1;


很好理解吧,当前元素比查找元素大则我们在查找元素的右边,反之亦然。

那么有同学会注意到,为什么 l 和 r 要 +1,-1呢?

假定此时 l=2,r=3,得 mid=2 ,如果此时不是查找元素的话,那么如果当前元素比查找元素小,那么我们调整 l 的值。

如果不+1的话会发生什么呢?l 再次被赋值为2,则进入无限循环,所以我们需要+1,-1

则核心代码如下:

sort(a+1,a+n+1);
bool flag=false;
int l=1,r=n;
while(l<=r)
{
int mid=l+r >> 1;
if(a[mid]==m)
{
flag=true;
break;
}
if(a[mid]>m) r=mid-1;
else l=mid+1;
}


接下来我们举个栗子。

假定数组为

1 2 4 5 7 8 9 11


我们来找4这个元素

工程开始:

l=1 , r=8 , mid=4 , a[mid]>4 , r=mid-1=3

l=1 , r=3 , mid=2 , a[mid]<4 , l=mid+1=3

l=3 , r=3 , mid=3 , a[mid]=3 , 退出循环

工程结束。

真的很短很快有木有?!

那么二分查找可以应用在哪里呢?

LIS 及 LCS 的 Θ(nlogn) 版就会用到二分查找。

接下来就是更喜闻乐见丧心病狂的二分答案

EG 2 二分答案

例题:Luogu P2440 木材加工

原题链接

很多老师应该都会拿这道题当入门题吧。

看到这道题,大多数人的想法应该是暴力枚举切段长度吧。

但转头一看,原木的长度——

暴力,不存在的!

于是我们便要在答案可能在的范围中二分查找我们的答案了。

对这道题来说,答案的范围就是从1到 最长的原木长度。

于是,我们设 l=1 , r=maxn

好了,范围解决了,接下来又面临一个问题了,怎样判断答案可不可行呢?

没办法了,只能暴力了。

每根原木扫一遍,算出能切多少根,再加起来和 k 比较一下即可

判断的问题解决了,怎么判断下一步搜索的范围呢?

很显然,如果切得太少,那么长度太长,r=mid-1

如果切得太多,虽然达到了要求,此时我们可以记录答案,但长度可能太短,可能有更优解,那么 l=mid+1

代码如下:

#include<cstdio>
#include<algorithm>
using namespace std;
int n,k,a[100005],l,r,mid,ans,sum,maxn;
bool Lets_Cut(int m)
{
sum=0;//设置初值
for(int i=1;i<=n;i++) sum+=a[i]/m;//计算能切多少
if(sum>=k) return 1;//如果切够了返回true
else return 0;//不够返回false
}
int main()
{
scanf("%d %d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),maxn=max(maxn,a[i]);
l=1;
r=maxn;
while(l<=r)
{
mid=l+r >> 1;
if(Lets_Cut(mid))
{
ans=max(ans,mid);
l=mid+1;
}
else r=mid-1;
}//全部同上
printf("%d",ans);
return 0;
}


结果:

代码 C++,0.45KB

耗时/内存 0ms, 1738KB

总结

二分是OI中非常重要的一种优化的算法,可以优化非常巨量的时间复杂度,有很大的必要深入研究与练习

原创 By Venus

写的不好大佬轻喷
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  二分查找 算法