8.9 KiB
算法:二分答案 讲义
一、 引言
在基础二分查找中,我们的搜索域局限于已有的物理数组(如在一个有序序列中查找特定元素)。而二分答案是一种高阶的算法思维,它将二分的搜索域拓展到了抽象的解空间 中,用于锁定满足特定条件的最优解。
1. 经典最优化问题:砍树问题 (EKO)
问题描述:
给定 N 棵树的高度集合,需要收集至少 M 米的木材。设定一个全局锯片高度阈值 $H$,凡是高度大于 H 的树,其超出 H 的部分将被截取。
优化目标:在保证收集木材总量 \ge M 的前提下,求锯片高度 H 的最大可能值。
2. 最优化问题
日常算法中常见的“最大化最小值”、“最小化最大值”等最优化问题,直接正向求解往往极其困难,甚至缺乏解析解。 二分答案的思想提供了一种思路:它将**“求出最优解是多少”转化为了“验证某一个给定的猜测解是否合法”**。这种视角的转换,能够将指数级或多项式级的求解时间,降维至对数时间复杂度 $O(\log N)$。
二、 理论基础:单调性与复杂度突破
1. 暴力穷举的性能瓶颈
若采用暴力枚举,从高度 0 逐一递增尝试高度 $H$:
- 状态空间:最高树高若为
10^9米,则最坏需要尝试10^9次。 - 状态验证:每次尝试需遍历
N棵树验证总木材量,耗时 $O(N)$。 - 总时间复杂度:$O(N \times \text{MaxHeight})$。当
N \approx 10^6且\text{MaxHeight} \approx 10^9时,运算量达10^{15}级别,必定导致 TLE。
2. 二分充要条件:单调性 (Monotonicity)
观察砍树问题的内在物理规律:
- 阈值
H递增\rightarrow被截取部分缩短\rightarrow木材总量单调递减。 这种严格的单调递减特性,构成了二分答案的充要条件。在解空间[0, \text{MaxHeight}]中,必然存在一个明确的布尔值临界点(即true与false的分界线)。我们无需线性遍历,而是可以直接通过折半查找(二分法)快速逼近该临界点。 优化后的总时间复杂度将骤降至:$O(N \log (\text{MaxHeight}))$。
三、 标准求解模板
解决此类问题的标准步骤如下:
1. 界定解空间 [left, right]
明确答案的数学下界与上界。在砍树问题中:
left$= 0$(地平线)。right$= \max(\text{trees})$(树的理论最高值)。
2. 构造验证函数 check(mid)
设计一个返回布尔值的函数,负责回答判定性问题:“若猜测答案为 $mid$,当前方案是否合法?”
在砍树问题中,仅需计算高度为 mid 时的总木材截取量,与需求 M 进行比较即可。
3. 在二分中收敛答案
在 [left, right] 区间内不断取中值 mid 交由 check 验证,并基于验证结果收缩边界,最终逼近极限即为我们的答案。
四、 算法推演与代码实现
1. 状态转移演示
假设 $N=4, M=7$,树高集合为 ${20, 15, 10, 17}$。
初始状态:left = 0, right = 20, ans $= 0$。
| 迭代 | left |
right |
mid |
check(mid) 验证 |
判定结果 | 边界更新策略 |
|---|---|---|---|---|---|---|
| 1 | 0 | 20 | 10 | 截取量 = 10+5+0+7 = 22 \ge 7 |
true |
木材充足,尝试抬高锯片:ans=10, left=11 |
| 2 | 11 | 20 | 15 | 截取量 = 5+0+0+2 = 7 \ge 7 |
true |
木材刚好,尝试极值:ans=15, left=16 |
| 3 | 16 | 20 | 18 | 截取量 = 2+0+0+0 = 2 < 7 |
false |
木材不足,必须降低锯片:right=17 |
| 4 | 16 | 17 | 16 | 截取量 = 4+0+0+1 = 5 < 7 |
false |
木材不足,必须降低锯片:right=15 |
循环终止:此时 $left (16) > right (15)$,退出循环。最优解 $ans = 15$。
2. C++ 实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 验证器:检测在高度 h 时,木材总量是否达标
bool check(long long h, long long m, const vector<int>& trees) {
long long sum = 0; // 使用 long long 预防大数值累加溢出
for (int tree_height : trees) {
if (tree_height > h) {
sum += (long long)(tree_height - h);
if (sum >= m) return true; // 达标即刻返回
}
}
return sum >= m;
}
int main() {
int n;
long long m;
cin >> n >> m;
vector<int> trees(n);
int max_h = 0;
for (int i = 0; i < n; ++i) {
cin >> trees[i];
max_h = max(max_h, trees[i]);
}
// 解空间初始化
long long left = 0, right = max_h;
long long ans = 0;
// 二分求解主逻辑
while (left <= right) {
// 防止 left+right 数值溢出的写法
long long mid = left + (right - left) / 2;
if (check(mid, m, trees)) {
ans = mid; // 记录合法解
left = mid + 1; // 持续逼近上界 (求最大值)
} else {
right = mid - 1; // 方案非法,收缩上界
}
}
cout << ans << "\n";
return 0;
}
五、进击的奶牛 (Aggressive Cows) —— 最大化最小值模型
1. 数学模型
- 物理场景:给定一条直线上
N个不重复的坐标点(牛舍),需要从中挑出M个坐标(安置奶牛)。 - 最优化目标:最大化这
M个坐标中,任意相邻两个坐标之间距离的最小值。 - 术语定义:这类问题在算法竞赛中被称为**“最大化最小值”**问题。
2. 单调性与解空间界定 要使用二分查找,必须先证明问题在解空间上存在单调性。
- 问题:能否在给定的坐标中选出
M个点,使得任意两点间距 $\ge d$。 - 推导:如果在间距要求极其苛刻(极大)的情况下
P(d)为true,那么放宽要求,对于任意更小的间距 $d-1$,P(d-1)必然也为true。 - 解空间映射:随着距离
d的不断递增,命题P(d)的返回值序列必然呈现[true, true, ..., true, false, false, ...]的阶跃分布。 - 边界界定:
- 下界
left = 0(理论上的最小距离)。 - 上界
right = max_pos - min_pos(理论上的最大物理跨度)。 - 目标:寻找使
P(d)为true的右侧极限边界(即最后一个true)。
- 下界
3. 贪心验证 (check 函数) 的正确性证明
在 check(d) 函数中,我们使用了“排序 + 顺序贪心”的策略。很多同学会产生疑问:为什么第一头牛必须强行放在第一个牛舍(最左侧)?
- 贪心选择性质证明:
假设存在一种最优的安置方案,第一头牛没有放在第一个牛舍,而是放在了第
k个牛舍($k > 1$)。此时,如果我们把这第一头牛强行向左移动,挪到第 1 个牛舍,它与第二头牛的距离只会增加,而绝不会减少。 因此,将第一头牛紧贴最左侧放置,是为后续的牛预留最大生存空间的绝对安全选择(不劣于其他任何选择)。 - 线性扫描:在固定了第一头牛之后,我们只需从左向右进行一次
O(N)的线性扫描。只要当前牛舍与上一头牛的相对距离 $\ge d$,就立即安置下一头牛。这种“能放就立刻放”的局部最优策略,最终能推导出全局的最大放置数量。
4. C++ 核心代码
// 当规定的绝对安全距离为 d 时,能否成功安置 m 头牛?
bool check(int d, int m, const vector<int>& stalls) {
// 前提:stalls 数组在传入前必须已经过升序排序!
int cows_placed = 1; // 直接在最左端安置第一头牛
int last_pos = stalls[0]; // 记录上一头牛的坐标
for (int i = 1; i < stalls.size(); ++i) {
// 只要物理距离达标,立即执行安置(局部最优解)
if (stalls[i] - last_pos >= d) {
cows_placed++;
last_pos = stalls[i]; // 更新
}
}
// 若实际安置总数大于等于目标需求 m,则当前距离 d 合法
return cows_placed >= m;
}
六、两类求解最值的模板
二分答案在算法竞赛中高度模式化,主要分化为两大结构。
1. 最大化合法解 (求满足条件的最大值)
解空间的布尔分布呈:[T, T, ..., T, F, F, ...]
目标:寻找最后一个 true。
- 边界更新:
check成功时记录ans,并执行left = mid + 1贪心向右探索。
2. 最小化合法解 (求满足条件的最小值)
解空间的布尔分布呈:[F, F, ..., F, T, T, ...]
目标:寻找第一个 true。
- 边界更新:
check成功时记录ans,并执行right = mid - 1贪心向左探索。