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

没错!你没有看错!
我来写那个让我讨厌至极的二分了!
二分真的很迷,有的时候必须用二分的地方看不出要用二分,然后就一片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为当前查找的位置。
这边有两种写法:

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
写的不好大佬轻喷

    原文作者:查找算法
    原文地址: https://blog.csdn.net/DKACVenus/article/details/79069128
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞