【二分答案】-----算法基本原理

二分答案算法详解:最优化问题中的判定法与单调性剖析


引言

在解决某些「最优化问题」时,我们无法直接枚举所有可能值或暴力模拟所有解。此时,一种强大且常用的技巧是二分答案法(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 函数 + 单调性 + 模板结构"三大核心,遇到任何"最小化最大值"或"最大化最小值"的优化问题,都可以第一时间联想到使用二分答案法,从而优雅、高效地求解复杂问题。