目录

一、二分法到底是什么?

二、二分法的优势

三、二分法的四大金刚

1. 精确查找

2. 左边界查找

3. 右边界查找

4. 特殊场景

四、二分法心法口诀

五、二分法例题讲解(附思路)

P2678 [NOIP 2015 提高组] 跳石头

P1182 数列分段 Section II

六、写二分算法的三个忠告


前言:二分法是非常高效的查找方法,在本篇学习结束后,可以掌握绝大多数情况的二分(一定要配合刷题进行巩固啊),同时有兴趣还可以了解一下C++STL中的lower_bound() 和 upper_bound(),相信我学完会有奇效的!!

附赠链接:【C++】STL模板库的lower_bound() 和 upper_bound() 最全使用指南(一篇教会你常用方法!!) -CSDN博客

一、二分法到底是什么?

打个比方,你玩"猜数字"游戏,对方想的是1-100之间的数字。每次你猜中间值50,对方告诉你"大了"或"小了",然后继续猜剩余区间的中间值——这就是最朴素的二分思想。

正式定义
二分法(Binary Search)是一种在有序集合中快速定位目标的算法,通过每次比较将搜索范围减半,直到找到目标或确定不存在。

这里有个需要注意的地方:二分法一定要在有序集合中,例如在用二分法找数组中的数时,必须将数组排序!!!

三大核心特征

  1. 数据必须有序(或存在单调性)

  2. 存在明确的分界条件

  3. 可以通过中间值判断排除一半数据


二、二分法的优势

为什么非要学二分法?难道我直接遍历数组或者集合去找需要的值不行吗?

可以,当然可以,但是当数量比较大的时候,直接暴力遍历的方法容易使得代码效率低下。例如当刷算法题的时候遇到数组长度比较大的时候,直接暴力寻找肯定是行不通的,容易TLE

先看对比实验
在10亿有序数据中查找一个数:

  • 普通遍历:平均5亿次比较 → 按每秒百万次计算,需要83分钟

  • 二分查找:最多30次比较 → 0.00003秒完成

时间复杂度

  • 最坏情况 O(log n)

  • 每次操作将数据规模n除以2,直到1为止

  • log₂(10^9) ≈ 30,log₂(2^32)=32

指数爆炸的逆袭

数据规模 线性查找次数 二分查找次数
100 100 7
10,000 10,000 14
1,000,000 1,000,000 20

这就是为什么大厂面试必考二分法——海量数据处理的核心技能


想必大家在写二分题时总遇到为什么这里要 left<=right 才能过,为什么有时候 left = mid +1, right = mid - 1,有时候又 left = mid + 1,right = mid.......这样的困惑在我学二分的时候也经常遇到并且为之困惑,一下代码可以跑,一下又进入死循环一直跳不出。接下来我们就来解决这个困惑!!!


三、二分法的四大金刚

1. 精确查找

标准二分查找,左闭右闭区间 [left, right]

  • 初始化:left = 0, right = n - 1

  • 循环条件:left <= right

  • 更新方式:

    • 若目标在右侧,left = mid + 1

    • 若目标在左侧,right = mid - 1

int binarySearch(vector<int>& nums, int target) {
    int left = 0, right = nums.size() - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1;  // 搜索右半部分
        } else {
            right = mid - 1; // 搜索左半部分
        }
    }
    return -1;
}

2. 左边界查找

寻找第一个大于等于目标的位置(左闭右开区间) (适合存在重复元素的情况)

左闭右开区间 [left, right)

  • 初始化:left = 0, right = n

  • 循环条件:left < right

  • 更新方式:

    • 若目标在右侧,left = mid + 1

    • 若目标在左侧,right = mid

int lowerBound(vector<int>& nums, int target) {
    int left = 0, right = nums.size();
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] >= target) {
            right = mid;    // 保持右开,缩小右边界
        } else {
            left = mid + 1; // 向右搜索
        }
    }
    return left; // left即為第一个≥target的位置
}

找左边界相当于问:"目标值从哪里开始出现?

3. 右边界查找

寻找最后一个小于等于目标的位置 (左闭右开区间)

int upperBound(vector<int>& nums, int target) {
    int left = 0, right = nums.size();
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > target) {
            right = mid;    // 保持右开,缩小右边界
        } else {
            left = mid + 1; // 向右搜索
        }
    }
    return left - 1; // left-1即為最后一个≤target的位置
}

找右边界相当于问:"目标值到哪里结束为止?

4. 特殊场景

(旋转数组找最小值)

int findMin(const vector<int>& nums) {
    int left = 0;
    int right = nums.size() - 1;
    
    while (left < right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] > nums[right]) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    
    return nums[left];
}

这个算法专为旋转排序数组的最小值查找设计,通过二分法高效缩小范围,是处理旋转数组类问题的经典方法。


四、二分法心法口诀

  1. 区间定义要记牢:闭区间端点含,开区间端点飘

  2. 循环条件看区间:闭区间用<=,开区间用<

  3. mid计算防溢出:l + (r - l)/2 是王道

  4. 收缩方向看比较:大了往左压,小了往右跑

  5. 边界检查不能少:找到之后验正身,防止幽灵数据闹


五、二分法例题讲解(附思路)

例题:[P2678 NOIP 2015 提高组] 跳石头 - 洛谷

P2678 [NOIP 2015 提高组] 跳石头

题目背景

NOIP2015 Day2T1

题目描述

一年一度的“跳石头”比赛又要开始了!

这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。

为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。

输入格式

第一行包含三个整数 L, N ,M ,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证 L >= 1且 N >= M >= 0  。

接下来 行,每行一个整数,第 i 行的整数Di(0 < Di <L) , 表示第 i 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。

输出格式

一个整数,即最短跳跃距离的最大值。

输入输出样例 #1

输入 #1

25 5 2 
2
11
14
17 
21

 输出 #1

  4

说明/提示

输入输出样例 1 说明

将与起点距离为 ​2 和 14​ 的两个岩石移走后,最短的跳跃距离为 4​(从与起点距离 17​ 的岩石跳到距离 ​21 的岩石,或者从距离 21​ 的岩石跳到终点)。

#define int long long

int l, n, m;
int a[50005];

bool check(int mid) {
    int now = 0, nex = 0;
    int x = 0;//计数
    while (nex < n + 1) {
        nex++;
        if (a[nex] - a[now] < mid) {
            x++;
        }
        else {
            now = nex;
        }
    }
    if (x <= m) {
        return true;
    }
    else {
        return false;
    }
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> l >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    a[n + 1] = l;
    int ans = 0;
    int left = 1, right= l, mid;
    while(left <= right) {
        mid = (left + right) / 2;
        if (check(mid)) {
            ans = mid;
            left = mid + 1;
        }
        else {
            right = mid - 1;
        }
    }
    cout << ans << endl;
    return 0;
}

思路: 本题需要找到最短跳跃距离的最大值,符合二分的应用场景。我们可以套用二分模板,直接二分答案,即最短跳跃距离的最大值。这里需要注意的就是check()函数,通过check函数查找当前跳跃距离需要移走多少块石块,注意当x<=m时返回true。这是因为我们需要找到符合条件的最大值,所以当x=m时,我们仍然需要向右寻找有没有比当前更大的满足条件的值,这是本题的精髓所在。

P1182 数列分段 Section II

题目链接P1182 数列分段 Section II - 洛谷

本题就不附上题目了(markdown转过来好像有点问题.......),大家请移步到链接查看题目详情吧!!!

那我们直接上思路:这道题是一个典型的二分答案的题,我们要找它的每段和的最大值(看到数组的和我们想到了前缀和,当然这题不用前缀和也可以过),然后就是找二分的边界。每段和的最大值一定比该数组中的最大的一个数大,这个最大数就是二分的left,每段最大和一定不会超过数组中所有数的和,所以right为数组之和。之后就套用二分模板

int n, m;
int a[100005];
int b[100005];

bool check(int mid) {
    int k = 0;//记录被分了几组
    int left = 0, right = 0;
    while (right <= n) {
        right++;
        if (b[right] - b[left] > mid) {
            k++;
            left = right - 1;
            right--;//当满足if条件时,right在划分的组的右边缘+1的位置,此时如果不减那下一段开始则组的长度为2,可以自己模拟一下,当然也有其他的方式
        }
    }
    k++;//最后一段无论大不大于mid都要算上,是这个题的坑
    if (k <= m) {//当k==m时,依旧向左寻找有没有更小的值
        return true;
    }
    return false;
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> m;
    int l = 0, r = 0, mid;
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        r += a[i];
        l = max(l, a[i]);
        b[i] = b[i - 1] + a[i];
    }
    while (l < r) {
        mid = (l + r) / 2;
        if (check(mid)) {
            ans = mid;//记录满足条件的答案
            r = mid ;
        }
        else {
            l = mid + 1;
        }
    }
    cout << ans << endl;
    return 0;
}

总结:二分法要注意二分的边界条件,确认left和right,其次就是看该二分题需不需要记录本次满足条件的答案,还是跳出while循环时答案就是mid


六、写二分算法的三个忠告

  1. 先画图再写代码
    在纸上画出数组,标出l/r/mid的位置变化,能避免90%的边界错误

  2. 测试用例要够狠
    必须测试:空数组、单元素、全相同元素、目标不存在、目标在首尾等情况

  3. 不要死记硬背
    理解区间定义和排除逻辑比背模板更重要,遇到变种题才能灵活应对


最后:为什么你总是调不出二分法?

根据我的经验,常见死循环原因就这三个(大佬勿喷!!我是蒟蒻):

  1. 区间定义混乱(一会闭一会开)

  2. 指针移动时没彻底排除mid

  3. 没处理元素重复的特殊情况

二分虽然很好,但不是什么情况都能用的!!

对于能否二分,有一个界定标准:状态的决策过程或者序列是否满足单调性或者可以局部舍弃性。

常见应用场景:最小化最大值问题, 最大化最小值问题


讲了这么多,聪明的你应该对二分有了自己的认识,让我们打开心爱的编译器和OJ,亲自上手试一试吧!!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐