刨析二分查找本质:超越“单调性”的边界判定与竞赛实战
刨析二分查找本质:超越“单调性”的边界判定与竞赛实战
在算法竞赛(如 ICPC、PTA 天梯赛、洛谷题库)中,二分查找(Binary Search)是出场率极高且极易写出 Bug 的基础算法。大多数教材将二分查找定义为“在有序数组中寻找特定元素”,这种理解在工业业务代码中勉强够用,但在高强度的算法竞赛中是远远不够的。
二分的真正本质并非“单调性”,而是“边界判定”。本文将刨析二分的底层逻辑,提供绝对不会死循环的 C++ 竞赛模板,并深入分析“二分答案”这一高阶实战技巧。
一、 二分的本质:寻找性质的边界
假设我们有一个区间,区间内的元素并不一定具有严格的数值单调性,但存在某种判定性质(Predicate)。
如果这个区间可以通过该性质被完美划分为两半:
- 左半部分:全部满足该性质。
- 右半部分:全部不满足该性质。
那么,二分查找的作用,就是以 \(O(\log N)\) 的时间复杂度,精准地找出这个分界点。这就是为什么很多看似没有“顺序”的图论题或动态规划题,依然能嵌套二分的原因——只要你能构造出一个将搜索空间“一分为二”的判定条件,就能二分。
二、 整数二分的终极模板:告别死循环
在手写二分时,最痛苦的莫过于陷入 while(l < r) 的死循环,或者在 l = mid 还是 l = mid + 1 之间犹豫不决。
以下两套 C++ 模板通过严格的区间划分,彻底杜绝了边界问题。其核心在于:mid 的计算方式必须与区间的更新方式严格匹配。
模板 1:寻找左边界(满足条件的第一个元素)
当我们将区间 [l, r] 划分为 [l, mid] 和 [mid + 1, r] 时,使用此模板。
// 寻找满足 check(x) 的最小值(左边界)
int binary_search_left(int l, int r) {
while (l < r) {
int mid = l + r >> 1; // 等价于 (l + r) / 2
if (check(mid)) {
r = mid; // mid 满足条件,答案在 [l, mid] 中
} else {
l = mid + 1; // mid 不满足,答案在 [mid + 1, r] 中
}
}
return l;
}
模板 2:寻找右边界(满足条件的最后一个元素)
当我们将区间 [l, r] 划分为 [l, mid - 1] 和 [mid, r] 时,使用此模板。
避坑核心:此时计算 mid 必须加上 1,即 l + r + 1 >> 1。因为 C++ 的整数除法是向下取整的,如果 l = r - 1,且没有加 1,mid 会等于 l。若此时执行 l = mid,区间大小不变,程序将陷入死循环。
// 寻找满足 check(x) 的最大值(右边界)
int binary_search_right(int l, int r) {
while (l < r) {
int mid = l + r + 1 >> 1; // 必须加 1,防止向下取整导致的死循环
if (check(mid)) {
l = mid; // mid 满足条件,答案在 [mid, r] 中
} else {
r = mid - 1; // mid 不满足,答案在 [l, mid - 1] 中
}
}
return l;
}
三、 竞赛核心杀器:二分答案 (Bisection of Answer)
在洛谷和 PTA 的中高阶题目中,纯粹的“在数组中二分”极少出现。取而代之的是“二分答案”。
1. 题型特征
如果你在题目中看到以下字眼,基本可以直接确立“二分答案”的思路:
- “最大化最小值”(如:把木头锯成 \(K\) 段,让最短的那段尽可能长)。
- “最小化最大值”(如:将数组分为 \(M\) 个连续子数组,让和最大的子数组尽可能小)。
- 题目要求一个极值,直接正向推导极其困难,但如果我“猜”一个答案,去验证它是否合法,却很容易(验证复杂度通常为 \(O(N)\))。
2. 核心逻辑:从“求解”降维到“判定”
二分答案的做法,就是直接在可能的答案值域(例如 \([1, 10^9]\))上进行二分查找。
每次取一个中间值 mid,将求解最优解的问题,转化为一个非黑即白的判定问题:check(mid)。
实战案例分析:最小化最大值(如洛谷 P1182 数列分段)
要求将长度为 \(N\) 的数组分成 \(M\) 段,使得各段和的最大值最小。
- 直接求:状态转移复杂,动态规划可能超时。
- 二分答案:假设最大和为
mid。此时问题降维成:“在每段和都不超过mid的前提下,能否将数组分成的段数 \le M?” check函数实现:贪心地从左到右累加,一旦当前和加上下一个元素超过了mid,就截断开辟新的一段。最后统计总段数是否 \le M。这只需要 \(O(N)\) 的时间。
整体时间复杂度从不可接受的级别,瞬间降到了 \(O(N \log(\text{MAX_VAL}))\),这就是二分答案的降维打击能力。
四、 浮点数二分与精度控制
除了整数二分,计算几何或物理模拟类题目中常涉及浮点数二分(例如求二次函数的根,或求解某条射线的相交时间)。
浮点数二分不需要考虑边界死循环问题(不需要 \(+1\)),但核心在于精度控制。
double float_binary_search(double l, double r) {
// eps 一般取题目要求精度的后两位,例如要求保留 4 位小数,eps 就取 1e-6
const double eps = 1e-6;
while (r - l > eps) {
double mid = (l + r) / 2.0;
if (check(mid)) {
r = mid;
} else {
l = mid;
}
}
return l;
}
暴力替代法:为了防止某些题目极端数据下精度判定失效,很多顶尖选手会直接放弃 while (r - l > eps),直接固定循环次数(如 for (int i = 0; i < 100; i++))。二分 100 次足以在浮点数范围内达到惊人的精确度,且绝对不会超时。
五、 总结
二分查找在算法竞赛中的地位,类似于物理学中的牛顿定律——形式简单,但衍生出的应用无穷无尽。
剥开表象,二分的本质是在一维空间中利用 \(O(\log N)\) 的时间代价进行快速试错。掌握好 l + r >> 1 和 l + r + 1 >> 1 的边界选择,明确 check() 判定的单调性分界点,你就彻底掌握了解决“最值优化问题”最有效的一把钥匙。

浙公网安备 33010602011771号