还不会二分查找?看这一篇就够了

这篇具有很好参考价值的文章主要介绍了还不会二分查找?看这一篇就够了。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、整数二分

二分查找分为整数二分和浮点数二分,一般所说的二分查找都是指整数二分。

1.1 二分查找模板

满足单调性的数组一定可以使用二分查找,但可以使用二分查找的数组不一定需要满足单调性。

不妨假设我们找到了条件 C 1 C_1 C1,它和它的对立条件 C 2 C_2 C2 能够将数组 a a a 一分为二,如下图所示:

还不会二分查找?看这一篇就够了,DSA,# AcWing,c++,二分查找,算法

因为 C 1 C_1 C1 C 2 C_2 C2 互为对立,故总有 C 1 ∪ C 2 ≡ True ,    C 1 ∩ C 2 ≡ False C_1\cup C_2\equiv \text{True},\;C_1\cap C_2\equiv \text{False} C1C2True,C1C2False(用C++语言描述,就是 c1 || !c1 总是为真,c1 && !c1 总是为假)。换句话说, ∀   x ∈ a \forall \,x\in a xa x x x 至少满足 C 1 C_1 C1 C 2 C_2 C2 中的一个,且 x x x 不会同时满足 C 1 C_1 C1 C 2 C_2 C2

观察上图可以发现,索引 3 3 3 和索引 4 4 4 这两个位置都可以作为 C 1 C_1 C1 C 2 C_2 C2 的分界点。其中,索引 3 3 3 是红色区域的右边界,索引 4 4 4 是绿色区域的左边界。而我们接下来要讨论的二分查找模板就是用来寻找 C 1 C_1 C1 C 2 C_2 C2 的分界点的

1.1.1 寻找右边界的二分查找

前面说过, C 1 C_1 C1 C 2 C_2 C2 的分界点一共有两个,因此我们的整数二分查找模板也有两个。一个用来查找右边界(即左侧的分界点,对应索引 3 3 3),一个用来查找左边界(即右侧的分界点,对应索引 4 4 4)。这里首先介绍查找右边界的模板。

因为查找的是红色区域的右边界,所以先定义一个函数 check(i),其中参数 i i i 是索引。当 i i i 位于红色区域,即 0 ≤ i ≤ 3 0\leq i \leq 3 0i3 时,check(i) 为真;当 i i i 位于绿色区域,即 4 ≤ i ≤ 7 4\leq i \leq 7 4i7 时,check(i) 为假。

初始时设置左右两个指针分别位于数组的左右两端,每次循环时计算 m i d = l + r 2 mid=\frac{l+r}{2} mid=2l+r(至于 m i d mid mid 到底取多少稍后会说),然后判断 check(mid) 的值(为实现二分查找,我们需要确保每次缩小区间时答案都落在区间内。这样一来,当最终 l == r 时,l 就是我们需要的答案)。如果 check(mid) 为真,说明 m i d mid mid 位于红色区域,且 m i d mid mid 有可能就是右边界,因此接下来令 l = m i d l=mid l=mid 来缩小查找范围(因为我们要保证缩小后的区间仍然包含答案);如果 check(mid) 为假,说明 m i d mid mid 位于绿色区域,且 m i d mid mid 必不可能是红色区域的右边界,因为 m i d mid mid 最多是索引 4 4 4,因此令 r = m i d − 1 r=mid-1 r=mid1 来缩小查找范围。

接下来重点关注 m i d mid mid 到底该取多少。如果 m i d = l + r 2 mid=\frac{l+r}{2} mid=2l+r,其中的除法代表整除,在某一轮循环出现了 r − l = 1 r-l=1 rl=1,则 m i d = 2 l + 1 2 = l mid=\frac{2l+1}{2}=l mid=22l+1=l。若 check(mid) 为真,则更新后的区间仍然为 [ l , r ] [l,r] [l,r],这就会导致无限循环。事实上,只需要取 m i d = l + r + 1 2 mid=\frac{l+r+1}{2} mid=2l+r+1,若 check(mid) 为真,则 m i d = r mid=r mid=r,更新后的区间为 [ r , r ] [r,r] [r,r],循环结束。若 check(mid) 为假,则更新后的区间为 [ l , l ] [l,l] [l,l],循环结束。

寻找右边界的二分查找模板:

int right_bound(int l, int r) {
    while (l < r) {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

1.1.2 寻找左边界的二分查找

类似1.1.1节中的分析,因为查找的是绿色区域的左边界,所以先定义一个函数 check(i),其中参数 i i i 是索引。当 i i i 位于绿色区域,即 4 ≤ i ≤ 7 4\leq i \leq 7 4i7 时,check(i) 为真;当 i i i 位于红色区域,即 0 ≤ i ≤ 3 0\leq i \leq 3 0i3 时,check(i) 为假。

初始时设置左右两个指针分别位于数组的左右两端,每次循环时计算 m i d = l + r 2 mid=\frac{l+r}{2} mid=2l+r(至于 m i d mid mid 到底取多少稍后会说),然后判断 check(mid) 的值。如果 check(mid) 为真,说明 m i d mid mid 位于绿色区域,且 m i d mid mid 有可能就是左边界,因此接下来令 r = m i d r=mid r=mid 来缩小查找范围;如果 check(mid) 为假,说明 m i d mid mid 位于红色区域,且 m i d mid mid 必不可能是绿色区域的左边界,因为 m i d mid mid 最多是索引 3 3 3,因此令 l = m i d + 1 l=mid+1 l=mid+1 来缩小查找范围。

接下来重点关注 m i d mid mid 到底该取多少。如果 m i d = l + r 2 mid=\frac{l+r}{2} mid=2l+r,其中的除法代表整除,在某一轮循环出现了 r − l = 1 r-l=1 rl=1,则 m i d = 2 l + 1 2 = l mid=\frac{2l+1}{2}=l mid=22l+1=l。若 check(mid) 为真,则更新后的区间为 [ l , l ] [l,l] [l,l],循环结束。若 check(mid) 为假,则更新后的区间为 [ r , r ] [r,r] [r,r],循环结束。综上所述, m i d mid mid l + r 2 \frac{l+r}{2} 2l+r 即可。

寻找左边界的二分查找模板:

int left_bound(int l, int r) {
    while (l < r) {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    return l;
}

1.2 应用:寻找元素的起始位置和终止位置

原题链接:AcWing 789. 数的范围

a = [1, 3, 3, 3, 4] 为例,数字 3 3 3 的起始位置和终止位置分别是 1 1 1 3 3 3(索引)。如何利用1.1节中的模板来完成此题呢?

现对数组 a a a 作出划分,若令 C 1 : a [ i ] < x ,    C 2 : a [ i ] ≥ x C_1:a[i]<x,\; C_2:a[i]\geq x C1:a[i]<x,C2:a[i]x(注意必须保证 C 1 , C 2 C_1,C_2 C1,C2 互为对立),则求 x x x 的起始位置便转化成了求 C 2 C_2 C2 区域的左边界。若令 C 1 : a [ i ] ≤ x ,    C 2 : a [ i ] > x C_1:a[i]\leq x,\; C_2:a[i]> x C1:a[i]x,C2:a[i]>x,则求 x x x 的终止位置便转化成了求 C 1 C_1 C1 区域的右边界。

在求 C 2 C_2 C2 区域的左边界时,check(mid) 即为 a[mid] >= x。在求 C 1 C_1 C1 区域的右边界时,check(mid) 即为 a[mid] <= x

AC代码如下:

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int a[N];

int left_bound(int l, int r, int x) {
    while (l < r) {
        int mid = l + r >> 1;
        if (a[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return l;
}

int right_bound(int l, int r, int x) {
    while (l < r) {
        int mid = l + r + 1 >> 1;
        if (a[mid] <= x) l = mid;
        else r = mid - 1;
    }
    return l;
}

int main() {
    int n, q;
    cin >> n >> q;
    for (int i = 0; i < n; i++) cin >> a[i];

    while (q--) {
        int k;
        cin >> k;
        int begin = left_bound(0, n - 1, k);
        if (a[begin] != k) cout << "-1 -1" << endl;
        else cout << begin << " " << right_bound(0, n - 1, k) << endl;
    }

    return 0;
}

二、浮点数二分

2.1 浮点数二分模板

相比整数二分,浮点数二分就要简单许多了,因为浮点数二分不涉及到边界问题。

浮点数二分通常用来求某个数 x x x 的近似值 x x x 不易直接求得,例如 x = 2 x=\sqrt{2} x=2 等)。由于此时左右两个指针也均为浮点数,所以我们不能直接判断 l == r,而是判断 r - l 是否小于预先设定的精度。

模板如下:

double fbsearch(double l, double r, double eps) {
    while (r - l > eps) {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

一些注意事项:

  • 若要求精确到小数点后 k k k 位,则 e p s eps eps 1 0 − ( k + 2 ) 10^{-(k+2)} 10(k+2);
  • m i d ≥ x mid\geq x midx,则 check(mid) 为真,否则为假。

2.2 应用:数的三次方根

原题链接:AcWing 790. 数的三次方根

注意到当 ∣ x ∣ < 1 |x|<1 x<1 时,有 ∣ x 1 / 3 ∣ > ∣ x ∣ |x^{1/3}|>|x| x1/3>x,故选取 l , r l,r l,r 时一定要谨慎。

因为 1000 0 1 / 3 < 22 10000^{1/3}<22 100001/3<22,所以这里取 l = − 22 ,   r = 22 l=-22,\,r=22 l=22,r=22 即可。

#include <iostream>

using namespace std;

double x;

double fbsearch(double l, double r, double eps) {
    while (r - l > eps) {
        double mid = (l + r) / 2;
        if (mid * mid * mid >= x) r = mid;
        else l = mid;
    }
    return l;
}

int main() {
    cin >> x;
    printf("%lf", fbsearch(-22, 22, 1e-8));
    return 0;
}

三、使用STL进行二分查找

3.1 std::binary_search

template< class ForwardIt, class T >
bool binary_search( ForwardIt first, ForwardIt last, const T& value );

该函数用来检查 [first, last) 区间内(该区间已排序)是否有数字等于 value,如果有返回 true,否则返回 false

3.2 std::lower_bound

template< class ForwardIt, class T >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T& value );

该函数用来返回 [first, last) 区间内(该区间已排序)首个大于等于 value 的元素的迭代器,如果找不到这种元素则返回 last

3.3 std::upper_bound

template< class ForwardIt, class T >
ForwardIt upper_bound( ForwardIt first, ForwardIt last, const T& value );

该函数用来返回 [first, last) 区间内(该区间已排序)首个大于 value 的元素的迭代器,如果找不到这种元素则返回 last

3.4 std::equal_range

template< class ForwardIt, class T >
std::pair<ForwardIt, ForwardIt>
    equal_range( ForwardIt first, ForwardIt last, const T& value );

该函数用来返回 [first, last) 区间内(该区间已排序)所有等于 value 的元素的「范围」。

「范围」实际上是由两个迭代器构成的 pairpair 中的第一个元素是 std::lower_bound 的返回值,pair 中的第二个元素是 std::upper_bound 的返回值。


接下来我们用STL来简化1.2节中的代码。

注意到对于数组而言,其迭代器就是指针,因此我们可以通过将返回的迭代器与数组名作差来得到迭代器所指向的元素的下标。

简化后的代码如下:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;

int a[N];

int main() {
    int n, q;
    cin >> n >> q;
    for (int i = 0; i < n; i++) cin >> a[i];

    while (q--) {
        int k;
        cin >> k;
        auto p = equal_range(a, a + n, k);
        if (*p.first != k) cout << "-1 -1" << endl;
        else cout << p.first - a << " " << p.second - a - 1 << endl;
    }

    return 0;
}

References

[1] https://www.acwing.com/activity/content/punch_the_clock/11/
[2] https://www.acwing.com/blog/content/31/
[3] https://zh.cppreference.com/w/cpp/algorithm/lower_bound文章来源地址https://www.toymoban.com/news/detail-804568.html

到了这里,关于还不会二分查找?看这一篇就够了的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SourceTree使用看这一篇就够了

     你梦想有一天成为git大师,然而面对复杂的git命令,你感觉TMD这我能记得住吗?你曾经羡慕从命令行敲git命令,才会更加炫酷,然而时间一长,TMD命令我有忘了。那么今天我介绍的这款工具会让你从git命令中解救出来,这就是git可视化工具SourcTree。 事实上Git的功能十分强大

    2024年02月08日
    浏览(26)
  • CAS自旋锁,看这一篇就够了

    前序 时隔多年,杰伦终于出了新专辑,《最伟大的作品》让我们穿越到1920年,见到了马格利特的绿苹果、大利的超现实、常玉画的大腿、莫奈的睡莲、徐志摩的诗… 他说“最伟大的作品”并不是自己的歌,而是这个世界上最伟大的艺术作品们。 为什么要写CAS自旋锁呢?最近

    2023年04月08日
    浏览(19)
  • ElasticSearch常见用法,看这一篇就够了

    2024送书福利正式起航 关注「哪吒编程」,提升Java技能 文末送3本《一本书讲透Elasticsearch:原理、进阶与工程实践》 大家好,我是哪吒。 ElasticSearch是一款由Java开发的开源搜索引擎,它以其出色的实时搜索、稳定可靠、快速安装和方便使用的特性,在Java开发社区中赢得了广

    2024年03月19日
    浏览(33)
  • 超图(HyperGraph)学习,看这一篇就够了

    最近事多,好久没更新了,随便写写(Ctrl+V)点 一、超图定义 通常图论中的图,一条edge只能连接2个vertex,在超图中,不限量 如何理解呢,就用我正在做的KT问题来看:7道题目-7个顶点;4种概念-4条超边,其中第1,2,3题都是考察概念1的,则构建一个包含了这仨的超边,以此类

    2024年02月02日
    浏览(32)
  • Docker Volume 看这一篇就够了

    默认情况下,在容器内创建的所有文件都存储在可写容器层上。这意味着: 当该容器不再存在时,数据不会持续存在,并且如果另一个进程需要数据,则可能很难将数据从容器中取出。 容器的可写层与运行容器的主机紧密耦合。您无法轻松地将数据移动到其他地方。 写入容

    2024年02月02日
    浏览(56)
  • 关于Tacotron2看这一篇就够了

    文章来源 [1712.05884] NATURAL TTS SYNTHESIS BY CONDITIONING WAVENET ON MEL SPECTROGRAM PREDICTIONS 参考博客 声谱预测网络(Tacotron2) Tacotron2 论文 + 代码详解 Tacotron2讲解 论文阅读 Tacotron2 Tacotron2 模型详解 Tacotron2-Details 简介: The system is composed of a recurrent sequence-to-sequence feature prediction network that m

    2024年02月09日
    浏览(18)
  • Java迭代器详解,看这一篇就够了

    迭代器 是属于 设计模式 之一, 迭代器模式 提供了一种方法来顺序访问一个聚合对象中各个元素,而不保留该对象的内部表示。 1) Iterator对象 称为 迭代器 ,主要用于遍历 Collection集合 中的元素。 2)所有实现了 Collection接口 的集合类都有一个 iterator() 方法,用以返回一个

    2024年02月02日
    浏览(23)
  • C++异常处理详解 看这一篇就够了

    在程序运行的过程中,我们不可能保证我们的程序百分百不出现异常和错误,那么出现异常时该怎么报错,让我们知道是哪个地方错误了呢? C++中就提供了异常处理的机制。 throw : 当问题出现时,程序会抛出一个异常。这是通过使用 throw 来完成的。 catch : 在您想要处理

    2024年02月14日
    浏览(17)
  • 背包问题(动态规划看这一篇就够了)

    题目描述     一个旅行者有一个最多能装M公斤的背包,现在有n件物品,他们的重量分别是W1,W2,......,Wn,他们的价值分别是C1,C2,......,Cn,求旅行者能获得的最大总价值。 输入     第一行:两个整数,M(M=200)和N(N=30);     第2至N+1行:每行两个整数Wi,Ci,表示每

    2024年04月15日
    浏览(18)
  • 了解5G安全标准,看这一篇就够了

    随着移动通信系统在社会生活中的使用越来越广泛,特别是5G进一步以企业级应用作为核心应用场景,安全成为了包括5G在内的移动通信系统不可忽视的因素。本文梳理了全球主流移动通信标准化组织在安全方面的标准制定,从而可以快速了解5G协议层面对信息安全的考量。原

    2024年02月05日
    浏览(19)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包