算法之二分法(例子:数的范围,数的三次方根)

写在前面

今天学习了二分法,整理了一下笔记,觉得是一种非常实用的方法
首先,学习历程重重困难,文章一定会难以避免的冗长,所以在前面先附上我制作的思维导图,大家可以先点进去看,思维导图是用的知犀思维导图,不过我觉得好像不用下载那个点开链接输入密码后也可以看,我觉得思维导图的表现力应该比文字更好,所以大家可以先看这个思维导图,有个大致的思路
链接:https://www.zhixi.com/view/495d55c8
密码:4292
并且一些关于二分的题目也一并放在思维导图中啦!大家可以看一下!

正文

什么是二分

二分法是一种十分常见的算法,其意很简单明了,就是挑一半,挑一半,再挑一半。。。。。。
当然这么说可能有点抽象,那就举个例子:
比如要从0——100中找一个数(当然是随机的,就类似于猜数字),人家选好了一个数,让你去猜,聪明的你当然不可能从1一直猜到100,这样未免太暴力了,于是有一种更好的办法,那就是先猜50,也就是取一半,然后让对方回答是大了还是小了,这就是把100个数分成了两半,无论是大了还是小了,我们都会知道有答案的那一边(哦,除非答案就是五十),然后重复上次操作,如果答案在左边就猜25,否则猜75.。。。。。。这样一直重复下去,总可以猜出答案。
这就是所谓的二分,那我们再来看一下这个例子。首先,是从0——100随机出一个数,而0——100是一个单调的区间,另外假如我们要猜的数是70,那么我们第一次二分后的0——50这个区间不满足有答案的这个性质(答案不在这里面),所以我们舍弃了他,在此进行二分重复上述动作直至二分出答案。
当然我们要注意的是:有单调性的题一定可以二分,能二分的题不一定有单调性!
故而我们可以得出二分的条件:
1.确定一个区间,使得目标值必然在区间内
2.找到一个性质,使得具有二段型且答案最好是二段性的分界点
那二分是什么呢?
二分:在某一区间上存在某种性质,使得整个区间一分为二,一半区间内有该性质,一半区间内不满足该性质,那么二分就可以寻找性质的边界,是一种查找方式

如何二分

二分分为两种,一种是整数二分,一种是实数二分

整数二分(不连续,是由一堆整数组成)

那接下来就要说一说如何整数二分了,我们先把一段分成两段,通常是从中间开始找中间值判断
在这里插入图片描述

假如说我们要二分橙色边界点,我们先找中间值mid
在这里插入图片描述

当然这里只是我画的像是在分界点,这只是赶巧了而已,有很多情况橙色与绿色的范围差还是很大的
然后我们检查这个点满不满足橙色的性质(或是是不是满足绿色的性质),*如果满足橙色的性质(也就是不满足绿色的性质)*那因为我们是要找橙色的边界,故其边界一定是在mid的右边,当然mid也有可能是答案(嗯。。。看来这个图画的就是这种情况)所以我们更新l=mid,新区间变成了mid至r,后再次进行二分(不断二分),一直到只剩下一个单位的时候,它就是答案。
那么如果我们刚开始的那个mid点不满足橙色的性质(或是满足绿色性质)呢?
在这里插入图片描述

那么说明那个mid在绿色的范围里,答案一定在mid左边,且不包括mid故而我们更新的对象就变成了 r=mid-1 , 范围区间就变成了l至mid-1

当然在这里又有一个重点,那就是如果我们只是人畜无害的将mid=l+r/2的话,在这里会出现一个问题,那就是当二分至最后只有两个数时即l=r-1时我们再次二分,此时由于除法的向下取整,我们做mid=(l+r)/2时得到的答案是mid=l,也就是我们的更新条件为l=l,它的范围区间没有得到更新,下一次取中点值还是l=l,这样会无限递归下去,也就是边界问题,为了解决这类问题,我们让每次取中点时这样取:mid=(l+r+1)/2这样就可以避免出现这种情况,这样当我们二分至l=r-1时,我们的mid=2r/2=r,故而此时二分结束,答案就是这里的r
好了,恭喜你!现在完成了橙色分界点的探索!
那假如说我们要找绿色的分界点呢?
当然也是一样了!先找到中间值看它是否满足绿色的性质
如果那个中间值满足橙色的性质
在这里插入图片描述
那么由于我们找的是绿色的边界点,于是我们的边界点一定在mid的右边(不包括mid),故而我们的更新条件就是l=mid+1,然后更换新的区间再次进行二分即可
同样,如果我们的中间值满足绿色的性质
在这里插入图片描述
显然,我们的绿色边界在mid的左半边(也有可能是在mid值上),也就是我们需要更新r值,将它更新为r=mid

在这种方式中,当我们一直二分到l=r-1时,我们再次二分mid=(l+r)/2=l(向下取整)由于此时我们的更新条件要么是l=mid+1要么是r=mid;无论是哪种情况,最后都可以得到长度为一的,也就是最后的值(比如更新状态是l=mid+1时,由于mid=l故新的l=r,又因为r=r此时长度为一二分结束,r=mid时同理)故而我们不用考虑边界问题,mid的取值还是(l+r)/2即可
故而,当我们找橙色分界点时,更新状态为l=mid或r=mid-1(看满不满足性质条件判断更新条件来缩小范围),找中间值mid=(l+r+1)/2

我们找绿色分界点时,更新状态为l=mid+1或r=mid,找中间值mid=(l+r)/2

实数二分

实数二分由于实数有无限多个,所以二分会没完没了,所以一般我们会通过题目判断它的误差小于多少后就可以近似相同了,这时我们输出答案,因为我们随便两个实数中间有无限个,故而不存在边界问题,我们不需要考虑mid是应该等于(l+r)/2还是应该等于(l+r+1)/2直接通通看成(l+r)/2即可,不断二分,当区间范围很小时(看题目)我们输出答案就可以啦。

模板

通过上述分析,我们可以得到以下模板:

整数二分

bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

###实数二分

bool check(double x) {/* ... */} // 检查x是否满足某种性质

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

如何结束二分

当l不在小于r时停止二分:

while(l<r)
{
	//进行二分
}

停止时判断条件,如果他就是你要找的东西,那就可以,如果他不等于你要找的东西,那就说明这里面没有

例子

是骡子是马,拉出来溜溜,我们先来看几个题目加深影响

整数二分例子:数的范围

给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。

如果数组中不存在该元素,则返回 -1 -1。

输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。

第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 k,表示一个询问元素。

输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1 -1。

数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
题意比较好理解,就是说有这么一段数是按从小到大排列的数,后再给你一些数,对于每个数让你寻找这个数在这个数组的起始点和终点
在这里插入图片描述
故而我们可以发现,就约等于我们既要找左边的边界又要找右边的边界

为什么可以二分

首先的一个问题是为什么可以二分,首先就拿我们的这个图来举例,对于我们要寻找的3,在他的左边满足一个性质:全都小于3(求左边界);对于他的右边也满足一个性质:全部大于3(求右边界),ok这样我们就有了二段性,而且我们的目标值恰巧就在二段性的边缘,故而可以二分
而且该数组是从小到大升序排列 有单调性的一定可以二分

找一下左边界

假设我们要找的那一段数为x,则左端点就是大于等于x的第一个位置,我们找到一个中点mid,判断条件就是:q[mid]是否>=x

判断更新条件

如果要是q[mid]>=x,则由于我们要找的是左端点,那么我们的左端点一定在mid的左端(可能会包含mid)故而更新条件就是:r=mid
知道了这个之后我们就可以由上方的模板得到else的更新方式:l=mid+1
由这个更新方式我们就可以知道我们的mid的更新方式:mid=(l+r+1)/2
不断二分,当最后我们的l不再小于r时

判断是否输出

当最后我们的l不再小于r的那一刻,我们判断(此时l与r相同)q[l]是否==x
如果等于,那它就是边界,否则我们直接输出-1 -1即可,不需要再进行右边界的判断了

找一下右边界

由于我们此时已经找完了左边界了,故而我们一开始的搜索范围就缩减为[二分的左边界,n-1],判断条件就是:q[mid]<=x

判断更新条件

由于我们有了判断左边界的经验,故而我们根本就不需要思考了,直接套用模板的后半段就可以,也就是l=mid或者r=mid-1,mid的更新条件是mid=(l+r)/2;

判断是否输出

由于我们的左半端有输出,故而我们的右半段也一定可以输出,不需要考虑

代码实现
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int n,m,mid;
int q[N];
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=0;i<n;i++){
        scanf("%d",&q[i]);//输入
    }
    for(int i=0;i<m;i++){
        int x;
        scanf("%d",&x);//输入要找的数
        //判断左边界
        int l=0,r=n-1;
        while(l<r){
            mid=l+r>>1;
            if(q[mid]<x)l=mid+1;
            else r=mid;
        }
        if(q[l]==x){//判断最后二分后长度为一的那个点等不等于x
            printf("%d ",l);
            //如果等于则判断右边界
            r=n-1;
            while(l<r){
                mid=l+r+1>>1;
                if(q[mid]>x)r=mid-1;
                else l=mid;
            }
            printf("%d\n",l);
        }else{//如果左边界判断时最后的值不是要求的x,那么x不存在,就不用再求右边界了,直接输出即可
            printf("-1 -1\n");
        }
    }
    return 0;
}

实数二分的例子(数的三次方根)

给定一个浮点数 n,求它的三次方根。

输入格式
共一行,包含一个浮点数 n。

输出格式
共一行,包含一个浮点数,表示问题的解。

注意,结果保留 6 位小数

数据范围
−10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000

为什么可以二分

此题有查找的性质,在题目要求的误差之下(误差在6位小数),满足在我们所求数的左边的数三次方小于n,而右边的三次方大于n
且实数是从小到大排的 有单调性的一定可以二分

判断输出

当我们二分至左右边界l-r<=10的负八次方时输出最为保险(题目误差再乘以十的负二次方)

代码实现
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int main()
{
    double n;
    scanf("%lf",&n);
    double l=-10000,r=10000;
    while(r-l>1e-8){//看题目给的误差范围再乘以10的负二次方保险
        double mid=(l+r)/2;
        if(mid*mid*mid>n)r=mid;
        else l=mid;
    }
    printf("%lf",l);
    return 0;
}

总结

好了这就是我们的二分
总结一下:
1.确定一个区间,使得目标值必然在区间内
2.找到一个性质,使得具有二段型
3.分析终点M在该判断条件下是否成立考虑更新条件
4.若更新R=mid,则不用做任何处理,若是L=mid,则需要在计算mid时加一

希望对大家有所帮助!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想进步的22级本科生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值