二分答案算法详解:最优化问题中的判定法与单调性剖析
引言
在解决某些「最优化问题」时,我们无法直接枚举所有可能值或暴力模拟所有解。此时,一种强大且常用的技巧是二分答案法(Binary Search on Answer)。
它的核心思想是:将原本求最小(大)可行值的问题,转化为一个关于"可行性"的判定问题,在有序区间上进行二分查找。
一、问题抽象:从最优化转化为判定问题
1.1 最优化目标
设有问题需要求出某个隐藏的最优解 V∗V^*V∗(最大或最小),例如:
- 给定一个数组,寻找最短子区间长度,使得其和 ≥ XXX;
- 给定若干位置,从中放置 kkk 个奶牛,使得最小相邻距离最大;
- 给定一个快乐值函数,求使"每日快乐值至少为 MMM"的最大可能 MMM。
这些问题都无法直接枚举答案,而是必须通过 判定"某个值是否可行" 来逆推出最优解。
1.2 判定函数(check)
我们构造函数 check(mid)
,判断在当前候选答案 midmidmid 下,是否存在可行解:
check(mid)
返回true
当且仅当存在某种操作使得最优值能达到或超过 midmidmid(或不超过,取决于方向)。
1.3 单调性要求
为使二分有效,check(mid)
必须具有单调性:
- 最大化最小值 时:
check(mid)
为true
⇒ 所有比它更小的值也为true
; - 最小化最大值 时:
check(mid)
为true
⇒ 所有比它更大的值也为true
。
这意味着,check
的结果在区间上形成一个"真 / 假"分段结构,具备标准二分查找前提。
二、二分答案模板
我们根据不同问题目标,设计两个对称的标准模板:
2.1 最大化最小值(返回最大可行的值)
cpp
#include <bits/stdc++.h>
using namespace std;
/*
* 二分答案模板:最大化最小值
* 定义:在整数区间 [L, R] 中寻找最大的 mid,使得 check(mid) == true。
*/
bool check(int mid)
{
// 在此处实现判定逻辑:如果存在方案能保证最小值 >= mid,返回 true;否则 false。
}
int main()
{
int L = /* 下界 */ , R = /* 上界 */;
int ans = L - 1; // 初始化为一个不满足的值
while (L <= R)
{
int mid = L + (R - L) / 2; // 防止溢出的中点计算
if (check(mid))
{
ans = mid; // mid 可行,记录并尝试更大值
L = mid + 1;
}
else
{
R = mid - 1; // mid 不可行,尝试更小值
}
}
cout << ans << "\n";
return 0;
}
2.2 最小化最大值(返回最小可行的值)
cpp
#include <bits/stdc++.h>
using namespace std;
/*
* 二分答案模板:最小化最大值
* 定义:在整数区间 [L, R] 中寻找最小的 mid,使得 check(mid) == true。
*/
bool check(int mid)
{
// 在此处实现判定逻辑:如果存在方案能保证最大值 <= mid,返回 true;否则 false。
}
int main()
{
int L = /* 下界 */ , R = /* 上界 */;
int ans = R + 1; // 初始化为一个不满足的值
while (L <= R)
{
int mid = L + (R - L) / 2; // 防止溢出的中点计算
if (check(mid))
{
ans = mid; // mid 可行,记录并尝试更小值
R = mid - 1;
}
else
{
L = mid + 1; // mid 不可行,尝试更大值
}
}
cout << ans << "\n";
return 0;
}
注意事项
ans
的初始值需设为不合法解,防止出错;- 使用
mid = L + (R - L) / 2
防止溢出; - 二分结束后
ans
即为最优解。
三、check 函数设计要点
3.1 本质思路
将"是否能达到 mid"问题转化为实际操作模拟,一般有两种实现方式:
- 贪心(如每次尽量放在最靠左的位置);
- 前缀和 / 双指针 / 模拟(根据题目逻辑逐步推演)。
三、复杂度与适用场景
3.1 时间复杂度
- 二分次数为 O(log(答案区间长度))O(\log(\text{答案区间长度}))O(log(答案区间长度));
- 每次判定代价为 O(f(n))O(f(n))O(f(n))(线性、贪心或前缀和);
- 总体复杂度为 O(f(n)⋅log(区间))O(f(n) \cdot log(\text{区间}))O(f(n)⋅log(区间))。
3.2 适用条件
- 存在一个明确的最优目标值;
- 可设计一个具有单调性的判定函数;
- 区间大小巨大或复杂,暴力不可行;
- 答案是离散整数 或连续实数(浮点精度需控制)。
四、常见变体
4.1 浮点数二分(误差最小)
若答案为实数,需使用:
cpp
while (R - L > eps)
{
double mid = (L + R) / 2;
if (check(mid)) R = mid;
else L = mid;
}
- 精度
eps
控制二分终止; - 最后返回
L
或(L+R)/2
皆可。
4.2 带边界条件的二分
有些题答案并非从 0 开始,需合理设定 L, R
:
- 例如最大值最小问题:
L = 1
,R = max_element(arr)
; - 机器调度问题:
L = max(任务耗时)
,R = 所有任务总耗时
。
五、常见题型与 check 模式对照表
问题类型 | 二分目标 | check(mid) 含义 |
常见方法 |
---|---|---|---|
最大化最小值 | 保证最小值 ≥ mid | 是否存在方案使得最小值 ≥ mid | 贪心 / 模拟 |
最小化最大值 | 控制最大值 ≤ mid | 是否存在方案使得最大值 ≤ mid | 贪心 / DP |
固定数量划分 | 尝试每段代价 ≤ mid | 是否能在 mid 范围内划出 ≤ k 段 | 计数 / 前缀和 |
滑窗类限制 | 子区间代价 ≤ mid | 是否存在子区间满足限制 | 单调队列 / 双指针 |
实数误差问题 | 误差 ≤ mid | 某种精度误差能否接受 | 二分 + 模拟 |
六、实践技巧与调试建议
6.1中点计算避免溢出
cpp
int mid = L + (R - L) / 2;
6.2提前返回优化
在 check
中一旦条件成立立刻返回 true
,提高效率。
6.3模拟小样本
人工构造小样例,列出答案与 check 的变化,验证单调区间是否正确。
6.4调试输出
调试时建议打印:
cpp
cout << "L=" << L << ", R=" << R << ", mid=" << mid << ", ans=" << ans << "\n";
有助于分析二分过程中状态的变化,定位逻辑错误。
八、总结
要点 | 内容 |
---|---|
本质 | 将"最优化问题"转化为"单调判定问题"并进行二分 |
判定函数 | check(mid) 判断在 mid 情况下是否满足要求 |
模板 | 最大化最小值 & 最小化最大值两类对称模板 |
难点 | check 的构造与单调性证明 |
实用性 | 高,在工程、竞赛、科研中广泛应用 |
只要掌握了"check 函数 + 单调性 + 模板结构"三大核心,遇到任何"最小化最大值"或"最大化最小值"的优化问题,都可以第一时间联想到使用二分答案法,从而优雅、高效地求解复杂问题。