算法 /
二分答案:先写判定,再找边界
二分答案不是在答案空间里碰运气:先证明单调性、写出 check 函数,再处理整数二分与溢出。
二分答案的常见误解,是把它当成一个可以套用的循环模板。真正需要先想清楚的,是这一步:
如果答案 可行,那么比它更宽松/更严格的答案是否也可行?
只有当这个问题有确定答案时,二分才成立。
把最优化问题变成判定问题
很多“最小化最大值”或“最大化最小值”的题目,直接求最优值很困难,但判断某个值 是否可行相对容易。
设判定函数为 。如果 在数轴上呈现单调性,例如:
那么就可以通过二分找到 第一次为真的位置。
| 题目形式 | 判定函数 | 答案 |
|---|---|---|
| 最小化最大值 | 能否让最大值不超过 | 最小的可行 |
| 最大化最小值 | 能否让最小值不小于 | 最大的可行 |
| 可行性分配 | 容量为 时能否完成 | 边界值 |
一个不依赖模板的写法
以“找到最小的可行值”为例,维护一个不可行下界和一个可行上界:
bool check(long long x) {
// 判断 x 是否可行
// ...
}
long long lo = L - 1; // 保证不可行
long long hi = R + 1; // 保证可行
while (hi - lo > 1) {
long long mid = lo + (hi - lo) / 2;
if (check(mid)) {
hi = mid;
} else {
lo = mid;
}
}
// hi 是第一个可行值
cout << hi << '\n';
这种写法的好处是边界含义明确:lo 始终不可行,hi 始终可行,循环结束时 hi 就是答案。
复杂度来自哪里
如果 check(x) 的复杂度是 ,二分的值域大小为 ,总复杂度就是:
这里的“值域大小”不一定是数组长度,可能是题目给出的数值上限。不要把二分次数写死成 32 次,除非你已经确认值域范围。
三个容易出错的点
- 单调性没有证明。 样例能过不代表
check满足单调性;先用小数据或交换论证检查。 - 区间端点含义混乱。 先决定
lo与hi哪边可行,再让循环不变式贯穿始终。 - 中间值写出溢出。 习惯使用
lo + (hi - lo) / 2,并注意check内部也使用 64 位整数。
二分答案本身只是搜索方式。真正决定正确性的,是判定函数和单调性。
小结
看到“最小化最大值”“最大化最小值”“最小可行代价”时,可以先问三个问题:
- 答案范围是什么?
- 如何判断一个答案是否可行?
- 这个判定是否单调?
三个问题都有清楚的回答之后,再写二分循环。