# 算法:二分答案 讲义 ## 一、 引言 在基础二分查找中,我们的搜索域局限于已有的物理数组(如在一个有序序列中查找特定元素)。而二分答案是一种高阶的算法思维,它将二分的搜索域拓展到了抽象的**解空间** 中,用于锁定满足特定条件的最优解。 ### 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++ 实现 ```cpp #include #include #include using namespace std; // 验证器:检测在高度 h 时,木材总量是否达标 bool check(long long h, long long m, const vector& 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 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++ 核心代码** ```cpp // 当规定的绝对安全距离为 d 时,能否成功安置 m 头牛? bool check(int d, int m, const vector& 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` 贪心向左探索。