蓝桥杯算法精讲:二分算法之二分查找深度剖析

目录

  • 前言
  • 一、二分算法
    • [1.1 二分查找](#1.1 二分查找)
      • [1.1.1 牛可乐和魔法封印](#1.1.1 牛可乐和魔法封印)
      • [1.1.2 A - B 数对](#1.1.2 A - B 数对)
      • [1.1.3 烦恼的高考志愿](#1.1.3 烦恼的高考志愿)
  • 结语

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、二分算法

二分算法是我觉得在基础算法篇章中最难的算法,二分算法的原理以及模板其实是很简单的,主要的难点在于问题中的各种各样的细节问题。因此,大多数情况下,只是背会二分模板并不能解决题目,还要去处理各种乱七八糟的边界问题

1.1 二分查找

在排序数组中查找元素的第一个和最后一个位置




算法原理

当我们的解具有二段性时,就可以使用二分算法找出答案:

  • 根据待查找区间的中点位置,分析答案会出现在哪一侧
  • 接下来舍弃一半的待查找区间,转而在有答案的区间内继续使用二分算法查找结果

模板

二分的模板在网上至少能搜出来三个以上。但是,我们仅需掌握一个,并且一直使用下去即可

为了防止溢出,求中点时可以下面的方式

  • mid = l + (r - l) / 2;

时间复杂度

每次二分都会去掉一半的查找区域,因此时间复杂度为logN

模板记忆方式

  1. 不用死记硬背,算法原理搞清楚之后,在分析题目的时候自然而然就知道要怎么写二分的代码
  2. 仅需记住一点,if/else 中出现 -1 的时候,求 mid 就 +1 就够了

二分问题解决流程

  1. 先画图分析,确定使用左端点模板还是右端点模板,还是二者配合一起使用
  2. 二分出结果之后,不要忘记判断结果是否存在,二分问题众多,一定要分析全面

STL中的二分查找

bash 复制代码
< algorithm >
  1. lower_bound:在 [first, last) 区间内,返回第一个大于等于 target 的元素的迭代器;若所有元素都小于 target,返回 last。时间复杂度:O(logN)
  2. upper_bound:在 [first, last) 区间内,返回第一个大于 target 的元素的迭代器;若所有元素都小于等于 target,返回 last。时间复杂度:O(logN)

二者均采用二分实现。但是STL中的二分查找只能适用于"在有序的数组中查找",如果是二分答案就不能使用,因此还是需要记忆二分模板

解法一:二分查找模板

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int n = nums.size();
        //若整个数组没有数,后面会发生越界访问
        if(n == 0) return {-1, -1};
        //初始化
        int left = 0, right = n - 1;
        //起始位置
        while(left < right)
        {
            int mid = (left + right) / 2;
            if(nums[mid] >= target) right = mid;
            else left = mid + 1;
        }
        //left 或 right所指的位置就有可能是最终结果
        if(nums[left] != target) return {-1, -1};
        int retleft = left;

        //终止位置
        left = 0, right = n - 1;
        while(left < right)
        {
            int mid = (left + right + 1) / 2;
            if(nums[mid] <= target) left = mid;
            else right = mid - 1;
        }
        //若有起始位置,一定有终止位置
        return {retleft, right};
    }
};

解法二:STL中的二分查找

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        //找到第一个 >= target 的迭代器
        auto left = lower_bound(nums.begin(), nums.end(), target);
        //迭代器越界或指向的元素不是target
        if(left == nums.end() || (*left) != target) return {-1, -1};
        //找到第一个 > target 的迭代器
        auto right = upper_bound(nums.begin(), nums.end(), target);
        return {
            //转化为下标
            (int)(left - nums.begin()), (int)(right - nums.begin() - 1)
        };
    }
};

一、解题思路

结合两个函数的特性,可直接推导目标位置:

  1. 起始位置:lower_bound 返回的迭代器,若该迭代器指向的元素等于 target,则为起始位置;否则说明数组中无 target,直接返回 [-1, -1]。
  2. 结束位置:upper_bound 返回的迭代器减 1,即为最后一个等于 target 的元素位置(前提是起始位置有效)。

二、代码详细解析
起始位置查找

  • left_it = lower_bound(nums.begin(), nums.end(), target):在整个数组中二分查找第一个 >= target 的元素。
  • 判空逻辑:如果 left_it 等于 nums.end()(所有元素都小于 target),或 *left_it != target(找到的是大于 target 的元素),直接返回 [-1, -1]。

结束位置查找

  • right_it = upper_bound(nums.begin(), nums.end(), target):找到第一个 > target 的元素。
  • 结束位置下标 = right_it - nums.begin() - 1(因为 right_it 是第一个大于 target 的位置,前一个就是最后一个等于 target 的位置)。

迭代器转下标 :通过 迭代器 - 容器.begin() 得到元素的下标,强制转换为 int 以匹配返回值类型。

1.1.1 牛可乐和魔法封印

牛可乐和魔法封印

没啥好说的,直接分析上模板

cpp 复制代码
#include<iostream>
using namespace std;

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

int binary_search(int x, int y)
{
    //初始化
    int left = 1, right = n;
    // 大于等于 x 的最小元素
    while(left < right)
    {
        int mid = (left + right) / 2;
        if(a[mid] >= x) right = mid;
        else left = mid + 1;
    }
    if(a[left] < x) return 0;
    int tmp = left;
    //小于等于 y 的最大元素
    left = 1, right = n;
    while(left < right)
    {
        int mid = (left + right + 1) / 2;
        if(a[mid] <= y) left = mid;
        else right = mid - 1;
    }
    if(a[left] > y) return 0;
    
    return left - tmp + 1;
}

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];
    int q; cin >> q;
    while(q--)
    {
        int x, y; cin >> x >> y;
        cout << binary_search(x, y) << endl;
    }
    return 0;
}

补充两点:

一:if(a[left] < x) return 0;if(a[left] > y) return 0;的作用
1. if(a[left] < x) return 0;

  • 执行时机:第一个二分循环(找≥x 的最小元素)结束后。
  • 二分循环的特性 :循环条件是left < right,退出时left == right,此时left是我们 "认为" 的「≥x 的最小位置」。
  • 判断的意义 :如果此时a[left] < x,说明整个数组的所有元素都小于 x(比如数组是[1,2,3,4,5],x=6),不存在≥x 的元素,自然也不存在 "≥x 且≤y" 的元素,因此直接返回 0。
  • 反例验证 (结合题目示例 1):
    若输入查询x=6, y=10,数组是[1,2,3,4,5]:
    第一个二分结束后left=5,a[5]=5 < 6,触发该判断,返回 0(正确,因为没有元素≥6)。

2. if(a[left] > y) return 0;

  • 执行时机:第二个二分循环(找≤y 的最大元素)结束后。
  • 二分循环的特性 :循环条件是left < right,退出时left == right,此时left是我们 "认为" 的「≤y 的最大位置」。
  • 判断的意义 :如果此时a[left] > y,说明整个数组的所有元素都大于 y(比如数组是[1,2,3,4,5],y=0),不存在≤y 的元素,因此直接返回 0。
  • 反例验证 (结合题目示例 1):
    若输入查询x=0, y=0,数组是[1,2,3,4,5]:
    第二个二分结束后left=1,a[1]=1 > 0,触发该判断,返回 0(正确,因为没有元素≤0)。

如果去掉这两行,会出现逻辑错误

比如数组[1,2,3,4,5],查询x=6, y=10:

  • 第一个二分后tmp=5(a[5]=5 <6);
  • 第二个二分后left=5(a[5]=5 ≤10);
  • 计算left - tmp +1 =5-5+1=1,但实际结果应该是 0,明显错误。

这两行判断正是为了堵住这类 "二分找到的位置不满足条件" 的漏洞,确保只有当「存在≥x 的元素」且「存在≤y 的元素」时,才会计算最终的个数。

二:日常开发中(32 位 int):取值范围在 109 量级(约 ±21 亿);

1.1.2 A - B 数对

A - B 数对

数据范围ai是0到230,加加减减有可能超过int的范围,所以可以用long long

由于顺序不影响最终结果,所以可以先把整个数组排序。

由A-B=C得:B=A-C,由于C是已知的数,我们可以从前往后枚举所有的A,然后去

前面找有多少个符合要求的B,正好可以用二分快速查找出区间的长度。

【STL的使用】

  1. lower_bound:传入要查询区间的左右迭代器(注意是左闭右开的区间,如果是数组就是左右指
    针)以及要查询的值k,然后返回该数组中≥k的第一个位置;
  2. upper_bound:传入要查询区间的左右迭代器(注意是左闭右开的区间,如果是数组就是左右指
    针)以及要查询的值k,然后返回该数组中>k的第一个位置;

要点补充:
sort(first, last) 会对 [first, last) 区间内的元素排序,包含 first 指向的元素,不包含 last 指向的元素

sort(a + 1, a + 1 + n) → 排序范围是 [a+1, a+1+n),对应数组元素 a[1], a[2], ..., a[n],正好是输入的 n 个元素。

i 从 2 开始的核心原因:i=1 时没有可匹配的 B 元素,无法形成数对,遍历无意义。避免一次无效循环,让代码更高效

STL二分算法解法

cpp 复制代码
#include<iostream>
#include<algorithm>
using namespace std;

const int N = 2e5 + 10;
typedef long long LL;
int n;
LL a[N];
LL c;

int main()
{
	cin >> n >> c;
	for(int i = 1; i <= n; i++) cin >> a[i];
	sort(a + 1,  a + 1 + n);
	LL ret = 0;
	for(int i = 2; i <= n; i++)
	{
		LL b = a[i] - c;
		ret += upper_bound(a + 1, a + i, b) - lower_bound(a + 1, a + i, b);
	}
	cout << ret << endl;
	return 0;
}


二分模板解法(不建议)

cpp 复制代码
#include<iostream>
#include<algorithm>
using namespace std;

const int N = 2e5 + 10;
typedef long long LL;
int n;
LL a[N];
LL c;

int main()
{
    cin >> n >> c;

    for (int i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + 1 + n);
    LL ret = 0;

    for (int i = 2; i <= n; i++)
    {
        LL b = a[i] - c;
        // 1. 找第一个 ≥ b 的位置(左端点模板),区间 [1, i-1]
        LL left = 1, right = i - 1; // 改用LL避免mid溢出
        while (left < right) {
            LL mid = (left + right) / 2;
            if (a[mid] >= b) {
                right = mid;
            }
            else {
                left = mid + 1;
            }
        }
        LL first_ge = left; // 第一个≥b的位置

        // 2. 找最后一个 ≤ b 的位置(右端点模板),区间 [1, i-1]
        left = 1, right = i - 1;
        while (left < right) {
            LL mid = (left + right + 1) / 2; // 右端点模板必须+1
            if (a[mid] <= b) {
                left = mid;
            }
            else {
                right = mid - 1;
            }
        }
        LL last_le = left; // 最后一个≤b的位置

        // 3. 严谨的匹配判断:区间有效 + 第一个≥b的元素就是b
        LL cnt = 0;
        if (first_ge <= last_le && a[first_ge] == b) {
            cnt = last_le - first_ge + 1;
        }
        ret += cnt;
    }
    cout << ret << endl;
    return 0;
}

说一下最后的if判断作用:
一、前置前提:两个二分的核心结果

在这段代码执行前,我们通过两个模板得到了两个关键位置:

  1. first_ge:数组 a[1~i-1] 中第一个 ≥ b 的元素下标(左端点模板结果)。
  2. last_le:数组 a[1~i-1] 中最后一个 ≤ b 的元素下标(右端点模板结果)。

因为数组是升序排序 的,如果存在等于 b 的元素,这些元素必然是一个连续的区间,且这个区间的左边界就是 first_ge,右边界就是 last_le。

二、代码逐部分拆解
1. 初始化计数:LL cnt = 0;

先默认当前 a[i] 作为 A 时,没有找到符合条件的 B,计数为 0,避免后续无匹配时出现垃圾值。
2. 核心条件:if (first_ge <= last_le && a[first_ge] == b)

这是双重校验 ,缺一不可,目的是排除所有 "假匹配" 情况,只保留真正存在 b 的场景。

3. 计数逻辑:cnt = last_le - first_ge + 1;

当双重校验通过后,说明 a[first_ge ~ last_le] 这个连续区间内的所有元素都等于 b(因为数组升序,且左边界≥b、右边界≤b)。

  • 公式意义:连续区间的元素个数 = 右边界 - 左边界 + 1(比如下标 1 到 2,是 2 个元素,2-1+1=2)。
  • 示例:样例中 i=3,b=1,first_ge=1,last_le=2,则 2-1+1=2,正好是 2 个 1,统计正确。
    4. 累加计数:ret += cnt;
    将当前 a[i] 作为 A 时找到的有效 B 的个数,累加到最终结果中。

1.1.3 烦恼的高考志愿

烦恼的高考志愿

【解法】

先把学校的录取分数「排序」,然后针对每一个学生的成绩,在「录取分数」中二分出≥b的「第一个」位置pos,那么差值最小的结果要么在pos位置,要么在pos一1位置。

取 abs(a[pos]一b)与abs(a[pos一1]一b)两者的「最小值」即可。

细节问题:

  • 如果所有元素都大于b的时候,pos一1会在0下标的位置,有可能结果出错;
  • 如果所有元素都小于b的时候,pos会在n的位置,此时结果倒不会出错,但是我们要想到这
    个细节问题,这道题不出错不代表下一道题不出错。

加上两个左右护法,结果就不会出错了。

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

typedef long long LL;
const int N = 1e5 + 10;
int n, m;
LL a[N];

int find(LL x)
{
    int left = 1, right = m;
    while(left < right)
    {
        int mid = (left + right) / 2;
        if(a[mid] >= x) right = mid;
        else left = mid + 1;
    }
    return left;
}

int main()
{
    cin >> m >> n;
    for(int i = 1; i <= m; i++) cin >> a[i];
    sort(a + 1, a + 1 + m);
    // 加上左右护法
    a[0] = -1e7 + 10;
    LL ret = 0;
    for(int i = 1; i <= n; i++)
    {
        LL b; cin >> b;
        int pos = find(b);
        ret += min(abs(a[pos] - b), abs(a[pos - 1] - b));
    }
    cout << ret << endl;
    return 0;
}

代码中数据类型用long long的原因:

题目中明确给出了数据范围:

学生数 n 和学校数 m 最大可达 105,学校分数线 ai​ 和学生估分 bj​ 最大可达 106,这意味着单个学生的 "不满意度"(即分数线与估分的绝对差)最大可达 106,所有学生的不满意度之和最大可达 105×106=1011

而在 C++ 中,int 类型通常是 32 位,其最大值约为 2.1×109,远小于 1011。如果用 int 存储总和 ret,必然会发生整数溢出,导致结果错误。因此,必须使用 64 位的 long long 类型来存储总和。

ret 变量:用于累加所有学生的不满意度,必须是 long long 类型,否则会溢出。

a 数组和 b 变量:虽然 ai​ 和 bj​ 的值(最大 106)用 int 也能存,但将它们定义为 long long 有以下好处:

  • 统一数据类型,避免在计算 abs(a[pos] - b) 时出现类型不匹配的问题。
  • 提高代码的兼容性,即使未来题目数据范围扩大,也无需修改类型定义。
  • 防止在极端情况下(如 ai 和 bj 接近 106),差值计算时出现溢出。

结语

相关推荐
电报号dapp1191 小时前
公链浏览器:区块链世界的“数据透视镜”与哈希查询的艺术
算法·区块链·智能合约·哈希算法
phltxy2 小时前
前缀和算法:从一维到二维,解锁高效区间求和
java·开发语言·算法
码上淘金2 小时前
Prometheus 瘦身指南:小白也能看懂的指标过滤与标签优化
java·算法·prometheus
tankeven2 小时前
HJ128 小红的双生排列
c++·算法
IronMurphy2 小时前
【算法二十二】 739. 每日温度 42.接雨水
算法
一轮弯弯的明月2 小时前
竞赛刷题-建造最大岛屿-Java版
java·算法·深度优先·图搜索算法·学习心得
黑眼圈子2 小时前
牛客刷题记录1
算法
祁同伟.2 小时前
【C++】哈希的应用
开发语言·数据结构·c++·算法·容器·stl·哈希算法
点云SLAM2 小时前
Tracy Profiler 是目前 C++ 多线程程序实时性能分析工具
开发语言·c++·算法·slam·算法性能分析·win环境性能分析·实时性能分析工具