二分算法详解:整数二分及浮点数二分算法(Binary Search)(含算法模板)

一、二分算法简介

当我们要从一个序列中查找一个元素的时候,最简单无脑的方法就是顺序查找法,但由于在大数据情况下爆炸的时间复杂度而舍弃。

最常见的方法是二分查找,也称折半查找(Binary Search),它是一种效率较高的查找方法。

最近偶然看到 『LeetCode』 讨论中的大佬总结的 二分查找从入门到入睡 ,虽然文章巨长,但总结的很全,一些边界问题讲的也很细,其中包括了Y总的二分思路,非常推荐看一看!!


二、 算法基本思想和流程(时间复杂度 O ( l o g n ) O(logn) O(logn)

  • 算法思想: 假设在闭区间 [l, r] 中寻找目标值 x ,二分的思想是每次将区间长度缩小一半,当l = r时,我们就找到了目标值。
  • 注意:
    • 二分的本质并不是单调性,二者并没有必要的关系。 因为数据有单调性一定可以二分,但可以二分的题目不一定非要有单调性。
      二分算法详解:整数二分及浮点数二分算法(Binary Search)(含算法模板)_第1张图片
    • 二分的本质在于:在区间 [l, r] 中有性质,使得区间可以一分为二,一半边区间满足该性质而另一半区间不满足性质, 这样的话二分算法可以寻找这个性质的边界(红色和绿色的边界都行,因为是整数二分所以两边界不重合)
  • 整数二分: 文章中的两个模板分别对应着二分红色边界和二分绿色边界。
    • 【模板一】二分红色边界
      二分算法详解:整数二分及浮点数二分算法(Binary Search)(含算法模板)_第2张图片
      • Step1——确定中间点mid = (l + r + 1) / 2
      • Step2——判断
        • mid 满足性质 check(mid) ,即 if(check(mid)) = True,则更新区间为 [mid, r],即令l = mid 即可;
        • mid 不满足性质 check(mid) ,即 if(check(mid)) = False,则更新区间为 [l, mid - 1],即令r = mid - 1 即可;
      • Step3——循环前两步,不断缩短空间,直到 l >= r,边界值为 l
    • 【模板二】二分绿色边界
      二分算法详解:整数二分及浮点数二分算法(Binary Search)(含算法模板)_第3张图片
      • Step1——确定中间点mid = (l + r) / 2 (上取整是为了防止死循环);
      • Step2——判断
        • mid 满足性质 check(mid) ,即 if(check(mid)) = True,则更新区间为 [l, mid],即令r = mid 即可;
        • mid 不满足性质 check(mid) ,即 if(check(mid)) = False,则更新区间为 [mid + 1, r],即令l = mid + 1 即可;
      • Step3——循环前两步,不断缩短空间,直到 l >= r,边界值为 l
  • 浮点数二分 :相对于整数二分更简单,无需考虑边界问题,理解了整数二分后,浮点数二分不成问题。

三、 整数二分模板(背诵)

【模板一】

  • 循环条件:l < r
  • 划分区间:[l, r][l, mid - 1][mid, r]
  • 更新操作:r = mid - 1或者l = mid
  • 注意:计算mid时为了避免死循环需要加1,即mid = l + r + 1 >> 1
bool check(int x) {/* ... */} // 检查x是否满足某种性质

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

【模板二】

  • 循环条件:l < r
  • 划分区间:[l, r][l, mid][mid + 1, r]
  • 更新操作:r = mid或者l = mid + 1
  • 注意:计算mid时不需要加1,即mid = l + r >> 1
bool check(int x) {/* ... */} // 检查x是否满足某种性质

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

3. 浮点数二分模板(背诵)

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

double bsearch(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)  // 两种写法:此时是用精度控制循环次数,直接控制循环100次也是OK的!
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

四、 使用模板的几个关键问题

① 如何选择用哪个模板?

做题的顺序首先是确定 check() 函数,再进行区间划分的分析,再确定使用哪个模板。

可以看到:当 l = mid 时,我们使用模板一,且 mid = (l + r) / 2 为下取整;当 r = mid 时,我们使用模板二,且 mid = (l + r + 1) / 2 需要上取整;

② 为什么模板一需要加上 ‘1’ ,即 ‘mid = (l + r + 1) / 2’,而模板二又不需要了 ?
  • 使用模板一时,若 l = r - 1,即 lr 只差 1 的时候,如果 mid = (l + r) / 2 下取整的话,结果是等于 l 的,则一旦 if(check(mid)) = True ,更新区间会一直陷入到 [mid, r] = [l, r] 死循环中。
  • 同理,使用模板二时,当 l = r - 1,即 lr 只差 1 的时候,如果 mid = (l + r + 1) / 2 下上取整的话,结果是等于 r 的,则一旦 if(check(mid)) = True ,更新区间会一直陷入到 [l, mid] = [l, r] 死循环中。

五、 应用:模板题

【整数二分 - 模板题】AcWing 789. 数的范围
【思路】想要找到目标值 x 的起始坐标,可理解成找到 ≥x 的最小值,再判断找到的边界值是否与 x 相等,若不相等返回 -1;同样,想要找到目标值 x 的终止坐标,可理解成找到 ≤x 的最大值,再判断找到的边界值是否与 x 相等,若不相等返回 -1

【C++代码】

#include 

using namespace std;

const int N = 100010;

int n, m;
int q[N];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    while (m -- )
    {
        int x;
        scanf("%d", &x);

        int l = 0, r = n - 1;
        while (l < r)
        {
            int mid = l + r >> 1;
            if (q[mid] >= x) r = mid;
            else l = mid + 1;
        }

        if (q[l] != x) cout << "-1 -1" << endl;
        else
        {
            cout << l << ' ';

            int l = 0, r = n - 1;
            while (l < r)
            {
                int mid = l + r + 1 >> 1;
                if (q[mid] <= x) l = mid;
                else r = mid - 1;
            }

            cout << l << endl;
        }
    }

    return 0;
}

【浮点数二分 - 模板题】AcWing 790. 数的三次方根

【C++代码】

#include 

using namespace std;

int main()
{
    double x;
    cin >> x;

    double l = -100, r = 100;
    while (r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if (mid * mid * mid >= x) r = mid;
        else l = mid;
    }

    printf("%.6lf\n", l);
    return 0;
}

2022年06月26日:补充二分“相等返回”的模板

【模板三】相等返回

  • 循环条件:l <= r
  • 划分区间:[l, r][l, mid - 1][mid + 1, r]
  • 更新操作:r = mid - 1或者l = mid + 1
  • 注意:计算mid时不需要加1,即mid = l + r >> 1
int bsearch(int l, int r)
{
    while (l <= r)
    {
        int mid = l + r >> 1;
        if (q[mid] == target) return mid;
        else if (q[mid] < target) l = mid + 1;
        else r = mid - 1;
    }
    return -1;
}

2022年06月30日:补充二分三个模板的理解

此处借鉴学习一下『 LeetCode 大佬 - yukiyama』 的总结表格,详解请参考:二分查找从入门到入睡
二分算法详解:整数二分及浮点数二分算法(Binary Search)(含算法模板)_第4张图片

  • Y总的两个二分模板使用的循环条件是 l < r,结束循环条件必定是相等 l = r 终止,且二分范围一般来讲是 [0, n - 1]n 为数组长度。本质上在二分的过程中,mid 并没有完全覆盖整个数组,这怎么理解呢?
    • 我们先看模板一,我们考虑一种情况,当数组中所有元素都不满足性质时,这时候 r 值会一直缩小直到 l = r = 0 ,而在这个过程中 mid 不会取到 0 值就返回了,也就是说返回的 l = 0 值并没有通过性质的判断,无法确定是否满足我们所设的二分性质,因为这个性质的边界是可能存在于小于 0 上的。因此,保险起见,需要对输出的 l 再进行一次判断即可。总结来说,Y总的模板一是左开右闭的。
    • 同理我们分析模板二,同样考虑一种情况,当数组中所有元素都不满足性质时,这时候 l 值会一直扩大直到 l = r = n - 1 ,而在这个过程中 mid 不会取到 n - 1 值就返回了,也就是说返回的 l = n - 1 值并没有通过性质的判断,无法确定是否满足我们所设的二分性质,因为这个性质的边界是可能存在于大于 n - 1 上的。因此,保险起见,需要对输出的 l 再进行一次判断即可。总结来说,Y总的模板二是左闭右开的。
    • 所以,用Y总的模板一和模板二的时候需要小心一下 l 是否落到范围边界上了,此时就需要再次判定一下。或者将范围扩大到 [-1, n - 1] (模板一)/ [0, n] (模板二)。
  • 我们用同样的思想去看一看模板三(相等返回),不同于前两个模板,模板三的退出循环条件必定是相错终止,即 l - r = 1 ,按上述方法去分析的话,我们会发现模板三的 mid 会覆盖整个数组元素,因此模板三是左闭右闭的,且由于更新操作为r = mid - 1或者l = mid + 1,因此计算mid时不需要加1mid = l + r >> 1 即可。

你可能感兴趣的:(算法,#,基础算法,算法,Binary,Search,二分算法,整数二分,浮点数二分)