二分算法全攻略:从查找数值到二分答案的质变
二分算法全攻略:从查找数值到二分答案的质变
蔚蓝WEILANX二分算法全攻略:从查找数值到二分答案的质变
在算法面试和机试(如 ECNU 考研机试)中,二分算法(Binary Search) 是出现频率极高、也是极容易写出 Bug 的基础算法之一。它的本质不仅仅是在有序数组中找一个数,更是利用单调性来缩小搜索范围的强大思维工具。
本文将带你从基础的“红蓝染色法”二分查找,进阶到化腐朽为神奇的“二分答案”。
一、 二分查找:红蓝染色法的统一逻辑
很多同学在写二分时会被 left <= right 还是 left < right 困扰。灵神推荐的开区间二分((left, right)) 能完美避开所有边界坑。
核心模板:寻找到第一个满足条件的下标
以寻找 lower_bound(第一个 $\ge x$ 的数)为例:
1 | public int lowerBound(int[] nums, int target) { |
经典案例:34. 在排序数组中查找元素的第一个和最后一个位置
- 思路:
- 第一个位置 =
lowerBound(target) - 最后一个位置 =
lowerBound(target + 1) - 1
- 第一个位置 =
- 注意点:如果
lowerBound(target)返回的下标越界,或者该下标处的值不等于target,则说明数组中不存在该元素。
二、 二分答案:化“找数”为“做题”
“花费一个 log 的时间,增加了一个条件。” 这是二分答案的核心魅力。如果题目求的是“最大值的最小”或“最小值的最大”,且具有单调性,那么二分答案就是必杀技。
核心逻辑:从虚拟数组中找界限
假设答案范围在 [low, high] 之间,我们可以构建一个虚拟的布尔数组:
[check(low), check(low+1), ..., check(high)]
由于单调性,这个数组必然呈现为 [T, T, ..., T, F, F, ..., F] 或反之。我们的任务就是在这个虚拟数组上做二分查找。
典型案例 1:求最小(满足条件的左边界)
- 核心代码:
1
2
3
4
5
6
7
8
9public int minEatingSpeed(int[] piles, int h) {
int left = 0, right = 1_000_000_000;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (check(mid, piles, h)) right = mid;
else left = mid;
}
return right;
} - 单调性:速度越快,耗时越短。
- check(mid):计算以速度
mid吃完所有香蕉的总用时 $\le H$。
典型案例 2:最大化最小值(求最大符合条件的下界)
- 核心代码:
1
2
3
4
5
6
7
8
9
10public int maxDistance(int[] position, int m) {
Arrays.sort(position);
int left = 1, right = position[position.length - 1] - position[0] + 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (check(mid, position, m)) left = mid;
else right = mid;
}
return left;
} - 核心思路:二分查找“最小磁力”。如果以距离
mid能放下m个球,说明mid可能还可以更大,因此更新left = mid。 - 注意点:
check函数通常使用贪心策略来放置球。
典型案例 3:最小化最大值(求最小符合特的上界)
- 核心代码:
1
2
3
4
5
6
7
8
9
10public int splitArray(int[] nums, int k) {
long left = Arrays.stream(nums).max().getAsInt() - 1;
long right = Arrays.stream(nums).sum() + 1;
while (left + 1 < right) {
long mid = left + (right - left) / 2;
if (check(mid, nums, k)) right = mid;
else left = mid;
}
return (int) right;
} - 核心思路:二分查找“各子数组和的最大值”。如果这个最大值
mid能让数组分割成 $\le k$ 个子数组,说明mid还可以更小,更新right = mid。
三、 实战高阶:ECNU2024-基建高手
这是今天遇到的一道极具代表性的机试题。
题目复盘
- 描述:有 $n$ 条生产线,第 $i$ 条线每 $a_i$ 分钟产出一块赤金。求产出至少 $k$ 块赤金的最早时间 $T$。
- 数据范围:$n, k, a_i$ 均可达 $10^9$。
- 为什么必须二分?
- 时间 $T$ 可能非常大(可达 $10^{18}$),求 $LCM$ 或暴力模拟必然超时或溢出。
- 随着时间 $T$ 的增加,产出总数 $f(T)$ 严格不减(单调性)。
Java 实现模板
1 | static void solve() throws Exception { |
四、 特殊二分场景:旋转数组与峰值
并非只有全有序数组才能二分,只要存在局部单调性或二段性即可。
旋转数组的最小值:153. 寻找旋转排序数组中的最小值
- 核心代码:
1
2
3
4
5
6
7
8
9
10public int findMin(int[] nums) {
int left = -1, right = nums.length - 1;
int target = nums[nums.length - 1]; // 以末尾元素为基准
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) right = mid;
else left = mid;
}
return nums[right];
} - 二段性:旋转数组可以分为两段有序序列。通过与
nums[n-1]比较,可以判定mid落在左半段(较大值区)还是右半段(最小值所在区)。
五、 总结:二分的三种境界
- 境界一:找数。在有序数组中找特定值,重点是边界控制。
- 境界二:求极限。
- 求最小(满足要求的第一个):对应
right = mid。 - 求最大(满足要求的最后一个):对应
left = mid。
- 求最小(满足要求的第一个):对应
- 境界三:判可行性(二分答案)。将难题转化为一个
check(x)函数,通过枚举(二分)答案来验证可行性。
💡 考研机试避坑贴士:
- 上界溢出:二分答案时,
high的设置要大胆(如 $10^{18}$),但low + high可能溢出,务必使用mid = low + (high-low)/2。 - 早停策略:在
check函数累加过程中,一旦满足条件立即return true,防止中间过程溢出。
希望这篇笔记能帮你打通二分的任督二脉!下次遇到 $10^9$ 级别的数据且有单调性,请直接开启二分模式。












