二分模版(整数二分和小数二分)

二分有 整数二分和小数二分两种

整数二分

整数二分的作用

在这里插入图片描述
假设红色区域 和 绿色区域 加起来 是我们的一个数组,其中数组的元素都为整数。

其中红色区域内部的数字 满足一种共同的性质,绿色区域内部的数字满足一种共同的性质,由于是整数二分,所以两个区域不是完全连着的。

只要数组当中能够通过两个性质划分开,那么就可以通过二分。

二分的用途是 让我们可以 快速的找到 两个性质的 边界点。
在这里插入图片描述
也就是此时用箭头标记的两个点

思路实现和代码实现

二分之所以快,是因为 它每次 会排除当前区域的 一半。

大致思路如下:

首先取 整个 原序列的 中点 下标mid(假设该数组下标区间为 [ l, r ] ),然后判断 mid这个数字 满足 这两个性质当中的那一个,通过判断的结果,更改我们的 l 和 r ,同时更新 mid,直到 l 和 r 相遇。

我们来模拟一下。

我们先来找我们的 左边界点。

有一个很关键的点,那就是 找哪边的边界点,就判断哪边的性质,其实判断另一边也行,但是新手的话,建议这样弄。

由于我们是找 左边界点,所以我们就 判断 mid 是不是满足 我们 红色区域数字的性质。

注意:这里的红色区域和绿色区域的实际大小并不一定是图中画 的看起来的 大小关系。

1.假设我们 的 mid 满足了红色区域,那么就证明 mid 在下列红色区域中。
在这里插入图片描述
由于这里并不知道 红色区域 有多长,所以mid 可能是 任意一个红色区域内的地方。

我们的任务是要找 左边界点,那么 发现 mid 左边的 值(不包含 mid)就不可能 是我们的 答案。

所以我们 要更新我们 的 左区间,此时 就需要 l = mid,因为此时 mid有可能是答案。


2.假设我们的mid 不满足红色区域内的性质,那么 说明 mid 此时在 绿色区域内,并且可能是任意地方。
在这里插入图片描述
此时会发现,我们找的左边界点 始终在 mid 的左边(结果不可能是mid).

那么此时就需要更新 我们 的右区间点,也就是 r = mid - 1; 这里减1的原因就是 由于是找 左边界点,所以 mid 不可能是我们的结果。


接着只需要重复此效果,直到 l >= r 时停止。

接着我们把刚才的找左边界点 的思路 转换到代码上。

在这里插入图片描述
其中 check 函数为 判断是否 满足 红色区域内的性质,满足则返回 true,否则返回false。

这里值得注意的是,计算 mid 的 方式 不是 l + r >> 1 而是 加了个 1,这个是因为 如果不加1,那么就会导致 死循环。

我们来看下面这个例子。
在这里插入图片描述

假设我们的此时 l 和 r 已经紧挨着了,那么正常如果 mid 向下取整的话,那么 mid 就会等于 left。

然而此时如果 满足性质的话,执行的 语句是 l = mid。

那么就会造成 left 永远是left ,left 和 right 将永远无法 等于,所以就陷入了死循环。

所以只要记住,一旦 满足条件后的条件是 l = mid,那么mid 就一定要向上取整。


接下来我们来看找 右边界点的 思路和代码。

其实跟找左边界点时一样。
在这里插入图片描述
找哪边,就记得判断哪边,现在我们在找右边的边界点,所以一会判断的是 满不满足右边的性质。

1.如果满足右边的性质,说明此时 mid 在绿色区域范围内。
在这里插入图片描述
那么mid 右边 的值 就会排除掉(不包含 mid),所以此时就需要更新 区间的右端点,由于mid 可能是结果,所以更新语句 为 r = mid;

2.如果此时不满足 绿色区域内的性质,则 mid 在红色区域内,此时 更新语句就为 l = mid + 1.

这里+1的原因 是因为 mid 不可能 是我们的答案。

代码如下:
在这里插入图片描述

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

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

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

输入格式

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

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

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

输出格式

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

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

数据范围

1 ≤ n ≤ 100000 1 \le n \le 100000 1n100000
1 ≤ q ≤ 10000 1 \le q \le 10000 1q10000
1 ≤ k ≤ 10000 1 \le k \le 10000 1k10000

输入样例:

6 3
1 2 2 3 3 4
3
4
5

输出样例:

3 4
5 5
-1 -1

思路
首先这个题目的每次询问是找 起始位置和 终止位置。

我们可以起始位置和终止位置都用一次二分来查找边界点。

我们以这个输入样例 为例子。

在这里插入图片描述
假设我要找 3 的 起始位置和 终止位置。
在这里插入图片描述
对于起始位置来说,我们可以把这个区间 按照此图的分法分为两个区域,然后找右边界点。

其中红色区域内数字的值都是 小于 3 的,绿色区域内的值都是 大于等于3的。

这时候我们只需要根据我们刚才讲的 写就行了。

在这里插入图片描述
对于终止位置来说,我们可以分为 图片当中的两个区域,然后找左边界点。

其中 红色区域内的值都是 小于等于3的,绿色区域内都是 大于3的。


代码
有了思路,代码就很轻松了。

准备阶段:
在这里插入图片描述
输入环节:
在这里插入图片描述
询问环节:
在这里插入图片描述
接下来就是刚才的思路了,我们先找 起始位置。

在这里插入图片描述

这里把3换成 x 就可以了。

相当于这里分成了 < x 和 >= x 两个区域,然后找右边界点。

在这里插入图片描述
首先先定义我们区间的 两个端点。
在这里插入图片描述

然后再根据我们刚才的思路,就可以成功找到起始位置。

当然前提 你数组里面得有这个值。

所以我们不能直接打印这个起始位置,因为不确定有没有,所以得判断一下。

在结束这个循环后,此时 l 和 r 是相等的,我们可以判断这个值是不是 x ,从而判断数组当中有没有 x。

在这里插入图片描述
如果不等于 x 说明此时 x 在数组中不存在,所以就直接输出 -1 -1 然后 continue 继续循环。

在找完了起始位置,接着来找终止位置。

在找终止位置前,我们先把起始位置先打印了。
在这里插入图片描述

因为一会我们要 二次利用到这个 l 变量,当然也可以重新创建一个变量,看个人喜好。

在这里插入图片描述

这里我们需要 将 r 重新定位到 原数组的右端点。

原本l 和 r 在我们找起始位置后,在能找到值的前提下,都会指向 x 。

此时我们需要将范围 扩大到 能够 容纳下终止位置的范围,所以可以将 r 移动到最右端。

接着我们 再利用之前找左边界 的思路。
在这里插入图片描述
在这里插入图片描述
注意由于 这里是 l = mid; 所以算 mid 的时候是向上取整。

最后打印l 即可,由于题目格式要求,需要记得换行。

完整代码如下:

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int n, q;
int a[N];

int main()
{
    scanf("%d%d", &n, &q);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    
    while (q--)
    {
        int x;
        scanf("%d", &x);
        
        int l = 0, r = n - 1;
        while (l < r)
        {
            int mid = (l + r) >> 1;
            if (a[mid] >= x) r = mid;
            else l = mid + 1;
        }
        if (a[l] != x)
        {
            printf("-1 -1\n");
            continue;
        }
        
        printf("%d ", l);
        r = n - 1;
        
        while (l < r)
        {
            int mid = (l + r + 1) >> 1;
            if (a[mid] <= x) l = mid;
            else r = mid - 1;
        }
        
        printf("%d\n", l);
    }
    return 0;
}


总结

四个步骤;

  1. 定区间:找到在哪个区间里找
  2. 划分性质:使得要找的目标值在两大性质的边界上
  3. 找哪边的边界,就判断哪边
  4. 如果是 l = mid,那么计算mid需要向上取整,mid = (l + r + 1) >> 1

单调可以二分,但二分不一定单调:

  • 只要能够用某一种性质将一个区间分成两块就可以进行二分
  • 单调一定能够利用性质将区间分成两部分

小数二分

整数二分 是找边界点,而小数二分找的是 近似值。

整数二分是在一个整型数组当中 查找,而小数二分是在数轴中 查找,都是每次可以排除一半的区间,只不过小数二分中while循环内的结束条件和整数二分不一样。

小数二分比较简单,我们直接上题目。


题目

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

输入格式

共一行,包含一个浮点数 n n n

输出格式

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

注意,结果保留 6 6 6 位小数。

数据范围

− 10000 ≤ n ≤ 10000 -10000 \le n \le 10000 10000n10000

输入样例:

1000.00

输出样例:

10.000000

输入环节:
在这里插入图片描述
在整数二分中,while 循环里面的 判断是 l < r,也就是 当 l 和 r 相等时会退出循环,但是在数轴上,一直取mid,是不可能相遇的,只能两个下标越来越近,所以我们可以规定他们 “多近” 的时候 再结束循环。

首先先定义左右两个端点,这里我们直接取 整个n 的左右边界(题目里的取值范围)。
在这里插入图片描述

然后我们可以在while 循环里面这么写

在这里插入图片描述
这里的意思就是 两个点只有 小于等于 这个 1e-8 才会停止循环,也就是说这个 1e-8其实就是我们的精度。

在确定精度的时候有一个小技巧,题目让你 最终打印几位小数,那么你就在此数字上加个2

比如这个题目中让你 打印 六位小数,所以就写的是 1e-8 。

接着就简单了,还是找 mid。

然后判断 mid 的值,根据 mid 的值在去调整 左右端点。
在这里插入图片描述
这里 不是写成 mid * mid * mid < n的原因是 如果mid 太大,那么三次方 就有可能 超出存储范围,当然这个题数据比较小, 写成三次方也是可以的,但是 写成这样更保险点。

最后打印我们的l 或 r ,都是可以的。
在这里插入图片描述

完整代码如下:

#include <iostream>
using namespace std;

double n;
int main()
{
    scanf("%lf", &n);
    double l = -1e4, r = 1e4;
    
    while (r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if (mid < n / mid / mid) l = mid;
        else r = mid;
    }
    
    printf("%.6lf", r);
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值