算法心传·二分查找与答案

一、二分查找(binary search)

1.引入

二分法Bisection method)是一种在单调有序的集合或函数中查找解的高效算法。

其核心思想是:

  • 每次将搜索空间分为两部分
  • 通过判断解的位置舍弃一半空间
  • 逐步缩小范围直至找到目标

时间复杂度分析:

  • 整数域:对于长度为N的区间,需要O(logN)次确定分界点
  • 实数域:通过判断区间长度(R-L)是否达到精度要求(如R-L≥eps)
  • 指定次数t:最终区间长度为初始长度L除以2^t
  • 总复杂度:O(二分次数 × 单次判定复杂度)

我们思考这么一个问题:

假设你有一本1000页的英文字典,要查找单词”algorithm“的页码:

  • 方法一:从第1页开始,一页一页翻找
  • 方法二:随机翻开一页,根据字母顺序决定往前或往后翻
  • 方法三:每次都翻到中间位置,根据比较结果排除一半页面

显然,第三种方法最高效——这就是二分查找的核心思想。

2.对比线性搜索

// 线性搜索:O(n)时间复杂度
int linear_search(int arr[], int n, int target) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == target) {
            return i;
        }
    }
    return -1;
}
  • 当n=1,000,000时,最坏情况需要比较1,000,000次
  • 对于有序数据,这种”盲目”的搜索存在巨大优化空间

而二分查找算法基于分治思想,将问题规模每次减半:

初始:在[0, n-1]区间查找
第1步:比较中间元素,排除一半区间
第2步:在剩下的一半区间重复此过程

直至找到目标或区间为空

查找目标值 23 在排序数组中的位置

数组:[2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
索引: 0 1 2 3 4 5 6 7 8 9

第1轮:left=0, right=9, mid=4 → arr[4]=16 < 23 → 查找右半区 [5,9] 第2轮:left=5, right=9, mid=7 → arr[7]=56 > 23 → 查找左半区 [5,6]
第3轮:left=5, right=6, mid=5 → arr[5]=23 == 23 → 找到目标!

只用了3次比较,线性搜索需要6次。

下表为对比二分对于线性搜索的复杂度:

数据规模n线性搜索O(n)二分查找O(log₂n)
1,0001,000次比较10次比较
1,000,0001,000,000次20次比较
1,000,000,00010⁹次30次比较

指数级的效率提升!

3.实现模板

int binary_search_first(int l, int r)
{
    int ans = r + 1; // 初始化为"无解"状态(超出范围)

    while (l <= r)
    {
        int mid = (l + r) >> 1; // 防止溢出的写法,普遍最优,快速幂有提

        // 核心逻辑:如果mid满足条件,记录答案并向左继续寻找更小的
        // check_first函数为自定义判断函数
        if (check_first(mid))
        {
            ans = mid;   // 记录当前可行解
            r = mid - 1; // 向左缩小范围,寻找更小的解
        }
        else
        {
            l = mid + 1; // 向右扩大范围
        }
    }

    return ans;
}

int binary_search_last(int l, int r)
{
    int ans = l - 1; // 初始化为"无解"状态(小于左边界)

    while (l <= r)
    {
        int mid = (l + r) >> 1;

        // 核心逻辑:如果mid满足条件,记录答案并向右继续寻找更大的
        // check_last函数为自定义判断函数
        if (check_last(mid))
        {
            ans = mid;   // 记录当前可行解
            l = mid + 1; // 向右缩小范围,寻找更大的解
        }
        else
        {
            r = mid - 1; // 向左扩大范围
        }
    }

    return ans;
}

double binary_search_real(double l, double r)
{
    // 确保左边界小于右边界
    if (l > r)
    {
        swap(l, r);
    }

    // 不断二分直到区间足够小,eps为自定精度要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;

        if (check_real(mid))
        {
            // 如果mid满足条件,尝试更小的值
            r = mid;
        }
        else
        {
            // 如果mid不满足条件,尝试更大的值
            l = mid;
        }
    }

    return l; // 返回左边界作为近似解
}

对于二分查找在STL库中的快捷使用请看算法象限·查找类算法的有序查找。

二、二分答案

1.什么是二分答案

1.1 核心思想

二分答案不是一种新算法,而是二分查找的一种应用方式。它将最优化问题转化为判定性问题

常规思路:尝试所有可能的答案 → 判断是否可行 → 找到最优解
二分答案:猜测一个答案 → 判断是否可行 → 调整猜测范围 → 快速找到最优解

1.2 适用问题特征

  • 最大值最小化(Minimizing the Maximum)
    • 例子:把数组分成k段,使得最大段的和最小
  • 最小值最大化(Maximizing the Minimum)
    • 例子:分配资源,使得最小资源量尽可能大

2.二分答案模板

int binary_search_min_answer(int l, int r) //模板1
{
    int answer = -1; // 存储最终答案

    while (l <= r)
    {
        int mid = (l + r) >> 1;

        if (check(mid))
        {
            // mid可行,尝试寻找更小的可行解
            answer = mid; // 记录当前可行解
            r = mid - 1;  // 向左搜索,尝试更小的值
        }
        else
        {
            // mid不可行,需要增大
            l = mid + 1; // 向右搜索
        }
    }

    return answer;
}

int binary_search_max_answer(int l, int r) //模板2
{
    int answer = -1; // 存储最终答案

    while (l <= r)
    {
        int mid = (l + r) >> 1;

        if (check(mid))
        {
            // mid可行,尝试寻找更大的可行解
            answer = mid; // 记录当前可行解
            l = mid + 1;  // 向右搜索,尝试更大的值
        }
        else
        {
            // mid不可行,需要减小
            r = mid - 1; // 向左搜索
        }
    }

    return answer;
}

我们不难发现,其实二分答案的代码与之前二分查找别无二致,核心还是在于怎么处理边界问题。

三、二分核心要点

1.如何确定二分的边界

// 通常有三种方式确定边界:

// 1. 理论边界(推荐)
int l = 最大单个元素值;   // 最小可能值
int r = 所有元素总和;    // 最大可能值

// 2. 数据范围边界
int l = 1;               // 根据题目要求
int r = 1e9;            // 根据数据范围

// 3. 极值边界
int l = 0;
int r = INT_MAX;

2.如何设计Check函数

Check函数是二分答案的核心,设计时需要:

  1. 正确性:准确判断mid是否可行
  2. 高效性:通常是O(n)或O(n log n)复杂度
  3. 边界处理:考虑极端情况

3.选择哪个模板

  • 模板1:当问题是”最小值”时使用(如:最小运载能力)
  • 模板2:当问题是”最大值”时使用(如:最大能分配的最小值)

示例:

weights = [1,2,3,4,5,6,7,8,9,10], days = 5

搜索过程:
left=10, right=55
mid=32 → 需要2天 → 可行 → right=31
mid=20 → 需要3天 → 可行 → right=19
mid=14 → 需要4天 → 可行 → right=13
mid=11 → 需要5天 → 可行 → right=10
mid=10 → 需要6天 → 不可行 → left=11

最终答案:11

四、常见问题及技巧

1.为什么二分答案可行

因为答案具有单调性:如果值X可行,那么所有≥X的值都可行(对于最小化问题)。

2.调试技巧

  1. 打印中间结果:查看每次mid和check结果
  2. 验证最终答案:用找到的答案重新验证
  3. 边界测试:测试最小/最大输入

3.总结

二分答案的本质是:

  1. 问题转化:最优化问题 → 判定性问题
  2. 利用单调性:答案区间具有单调性质
  3. 二分加速:O(n)的判定 + O(log n)的搜索

掌握二分答案的关键是:

  1. 准确设计Check函数
  2. 合理确定搜索边界
  3. 正确选择模板方向

二分答案的难点不在二分本身,而在Check函数的设计问题转化

等我有时间精选几道二分好题供大家真正上手二分查找和答案。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇