刷二分法的血泪教训和避坑指南

3 阅读9分钟

大家好,我是Fairy,一名打完蓝桥杯 C/C++ A 组的正在复盘的大一计科女生。

这几天在疯狂刷题突击期,我终于翻过了一座初学算法时最容易卡壳的大山——二分答案(Binary Search on Answer) 。

以前一看到长篇大论的题目就发憷,但经过大量真题的毒打后,我总结出了一套自己的“二分法肌肉记忆”。今天这篇文章,不讲空洞的理论,只分享我自己在考场上的一套实战解题SOP(标准作业程序) 。如果你也经常在二分法的边界条件里迷失,希望这篇复盘能帮到你。

一、 题眼锁定:什么时候该用二分法?

在刷题时,我给自己定了一个死规矩:只要在题目最后一句的问法中,看到“最大化最小值”或者“最小化最大值”(比如:最多能分多长、最少需要多少时间),不要犹豫,99% 是二分答案。

这种题目的特征非常明显:

  1. 直接去求答案(正向推导)极度困难,甚至要写极其复杂的 DFS。
  2. 但是,如果我随便“猜”一个答案,让你去验证这个答案行不行(逆向验证),却非常简单。
  3. 答案存在单调性(比如:木头切得越长,能分给的人就越少;时间给得越宽裕,越容易完成任务)。

只要满足这三点,直接启动二分法解题流程。

二、 我的解题起手式:先开大数组

面对二分题,我的代码习惯是绝对不在 main 函数里开数组。解题前,我都会在 #include 下方,先把所有需要的变量全部挂在全局区: #include #include using namespace std;

// 1. 无脑开全局大数组,防止栈溢出 // 2. 只要有求和、累加的操作,无脑上 long long long long a[200005]; long long N, M; 全局数组自带初始化为 0 的光环,这在后续写前缀和或者极值判断时,能避开无数个隐蔽的 Bug。

三、 稳住阵脚:先写 while 循环模板

我解题的顺序是倒着来的:先写盲猜答案的 while 模板,再去死磕 check 函数。

外层的 while 循环就像是一个“盲猜员”,它的任务就是在 [left, right] 区间里不断折半,寻找那个极限答案。这里的模板可以直接肌肉记忆: long long left = 1; long long right = 1e9; // 根据题目数据范围定,或者取 max_a long long mid = 0, ans = 0;

while (left <= right) { // 防溢出神仙写法:千万别写 (left+right)/2 mid = left + (right - left) / 2;

if (check(mid)) {
    ans = mid; // 记录当前靠谱的答案
    left = mid + 1; // 满足条件,尝试进一步压榨极限(视求最大还是最小而定)
} else {
    right = mid - 1; // 不满足条件,乖乖退让
}

} cout << ans << endl;

四、 全场核心:最考验逻辑的 check 函数

这也是我写这篇博客最想感叹的一点:整个二分答案的难点,全在 check 函数中! 它是整个代码求解的关键,也是最考验逻辑的点。

如果说外面的 while 是盲猜员,那 check 函数就是铁面无私的**“裁判员”**。它接过 mid(猜的答案),结合题意去模拟一遍流程,最后只返回 true(可行)或 false(不可行)。

在刷题过程中,我发现在 check 函数里最容易犯两个逻辑致命伤,我拿经典的**“分书/切木头”**题型来举例:

避坑 1:变量覆盖 vs 变量累加

假设题目要求每天看书时间不能超过 mid。在遍历数组时,经常会写错累加逻辑: bool check(long long mid) { long long today_time = 0; int days = 1; // 注意:只要做事,天数起步就是 1!

for (int i = 0; i < N; i++) {
    if (a[i] > mid) return false; // 单个任务直接爆表,绝无可能
    
    if (today_time + a[i] > mid) {
        // 超时了,推到第二天
        days++;
        today_time = a[i]; 
    } else {
        // 没超时,今天继续干!
        // 【致命坑点】:这里必须是 +=,绝不能写成 =,否则今天前面的进度全清零了!
        today_time += a[i]; 
    }
}
return days <= M; // 最后对比的,是天数有没有超标

} 太棒了!原来你是想写一篇纯粹的硬核技术复盘博客。这种“方法论”加“实战心得”的写法,在掘金上绝对是爆款潜质,因为你精准地切中了无数算法新手的痛点——知道要用二分,但就是写不对。

把你刚才的真实感悟,结合你这几天打磨出来的代码逻辑,我为你重新梳理了一篇技术极客风的掘金博客。这篇博客不仅有你的心路历程,还有极其标准的方法论总结。

你可以直接把下面的 Markdown 文本复制到掘金的编辑器里:


【算法破局】看到“最大的最小”,我直接掏出二分答案模板

你好,我是洵澈,一名正在准备蓝桥杯 C/C++ A 组的大一计科女生。

这几天在疯狂刷题突击期,我终于翻过了一座初学算法时最容易卡壳的大山——二分答案(Binary Search on Answer)

以前一看到长篇大论的题目就发憷,但经过大量真题的毒打后,我总结出了一套自己的“二分法肌肉记忆”。今天这篇文章,不讲空洞的理论,只分享我自己在考场上的一套实战解题SOP(标准作业程序) 。如果你也经常在二分法的边界条件里迷失,希望这篇复盘能帮到你。


一、 题眼锁定:什么时候该用二分法?

在刷题时,我给自己定了一个死规矩:只要在题目最后一句的问法中,看到“最大化最小值”或者“最小化最大值”(比如:最多能分多长、最少需要多少时间),不要犹豫,99% 是二分答案。

这种题目的特征非常明显:

  1. 直接去求答案(正向推导)极度困难,甚至要写极其复杂的 DFS。
  2. 但是,如果我随便“猜”一个答案,让你去验证这个答案行不行(逆向验证),却非常简单。
  3. 答案存在单调性(比如:木头切得越长,能分给的人就越少;时间给得越宽裕,越容易完成任务)。

只要满足这三点,直接启动二分法解题流程。


二、 我的解题起手式:先开大数组

面对二分题,我的代码习惯是绝对不在 main 函数里开数组。解题前,我都会在 #include 下方,先把所有需要的变量全部挂在全局区:

C++

#include <iostream>
#include <algorithm>
using namespace std;

// 1. 无脑开全局大数组,防止栈溢出
// 2. 只要有求和、累加的操作,无脑上 long long
long long a[200005]; 
long long N, M; 

全局数组自带初始化为 0 的光环,这在后续写前缀和或者极值判断时,能避开无数个隐蔽的 Bug。


三、 稳住阵脚:先写 while 循环模板

我解题的顺序是倒着来的:先写盲猜答案的 while 模板,再去死磕 check 函数。

外层的 while 循环就像是一个**“盲猜员”**,它的任务就是在 [left, right] 区间里不断折半,寻找那个极限答案。这里的模板可以直接肌肉记忆:

C++

long long left = 1; 
long long right = 1e9; // 根据题目数据范围定,或者取 max_a
long long mid = 0, ans = 0;

while (left <= right) {
    // 防溢出神仙写法:千万别写 (left+right)/2
    mid = left + (right - left) / 2; 
    
    if (check(mid)) {
        ans = mid; // 记录当前靠谱的答案
        left = mid + 1; // 满足条件,尝试进一步压榨极限(视求最大还是最小而定)
    } else {
        right = mid - 1; // 不满足条件,乖乖退让
    }
}
cout << ans << endl;

把这段骨架敲完,整道题的地基就打好了。接下来,迎接真正的挑战。


四、 全场核心:最考验逻辑的 check 函数

这也是我写这篇博客最想感叹的一点:整个二分答案的难点,全在 check 函数中! 它是整个代码求解的关键,也是最考验逻辑的点。

如果说外面的 while 是盲猜员,那 check 函数就是铁面无私的**“裁判员”**。它接过 mid(猜的答案),结合题意去模拟一遍流程,最后只返回 true(可行)或 false(不可行)。

在刷题过程中,我发现在 check 函数里最容易犯两个逻辑致命伤,我拿经典的**“分书/切木头”**题型来举例:

避坑 1:变量覆盖 vs 变量累加

假设题目要求每天看书时间不能超过 mid。在遍历数组时,经常会写错累加逻辑:

C++

bool check(long long mid) {
    long long today_time = 0;
    int days = 1; // 注意:只要做事,天数起步就是 1!
    
    for (int i = 0; i < N; i++) {
        if (a[i] > mid) return false; // 单个任务直接爆表,绝无可能
        
        if (today_time + a[i] > mid) {
            // 超时了,推到第二天
            days++;
            today_time = a[i]; 
        } else {
            // 没超时,今天继续干!
            // 【致命坑点】:这里必须是 +=,绝不能写成 =,否则今天前面的进度全清零了!
            today_time += a[i]; 
        }
    }
    return days <= M; // 最后对比的,是天数有没有超标
}
避坑 2:判断条件的“边界等号”

在写 check 函数时,必须像强迫症一样审视每一个 >>=。 例如在距离问题中,如果要求工位距离至少mid: // 错误写法:错杀了刚好等于 mid 的合法情况 if (pos[i] - last_pos > mid)

// 正确写法:既然是至少,等于也是合法的! if (pos[i] - last_pos >= mid)

五、 结语

从最初看着 vector 和指针发呆,到现在能熟练地把题目拆解成 二分模板 + 自定义 check 逻辑,这个过程虽然痛苦,但当绿色的 Accepted 亮起时,一切都值了。

总结我的二分法则:

  1. 看到“最大的最小” -> 启动二分。
  2. 全局大数组起手 -> 稳住心态。
  3. 盲敲 while 模板 -> 划定边界。
  4. 全神贯注梳理 check 逻辑 -> 抓准 >= 还是 >,厘清是 += 还是 =

(大一 CS 女生的算法打怪升级之路,持续更新中... 欢迎大佬们在评论区指正~)