二分答案专题实战:木材加工与砍树问题详解
一、二分答案核心思路
二分答案并非直接对数组进行二分,而是针对解空间进行二分。其本质是「二分答案 + 判定」。当问题的解具有单调性(即随着答案增大,判定结果呈现二段性)时,我们可以利用二分查找来快速定位最优解。这类方法特别适用于解决「最大值最小」或「最小值最大」的问题。
在实际应用中,我们不需要知道具体的解是什么,只需要判断某个候选值是否满足条件。如果满足,就尝试更大的范围;如果不满足,则缩小范围。这种思路能显著降低时间复杂度。
二、经典例题解析
1. 木材加工
题目描述 给定 $n$ 根原木,长度分别为 $a_1, a_2, \[dots], a_n$。需要切割出至少 $k$ 段长度为 $x$ 的木材。求 $x$ 的最大值。
解题思路 假设我们要切出的长度为 $mid$,那么每根原木能切出的段数是 $\lfloor a_i / mid \rfloor$。如果所有原木切出的总段数大于等于 $k$,说明 $mid$ 这个长度可行,可以尝试更大的长度;否则需要减小长度。 这里存在明显的单调性:长度越小,能切出的段数越多。因此可以使用二分查找。
代码实现
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL a[N];
int n;
LL k;
// 计算在切割长度为 x 的情况下能切几段
LL calc(LL x) {
LL cnt = 0;
for (int i = 1; i <= n; i++) {
cnt += a[i] / x;
}
return cnt;
}
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
// 二分范围:左边界为 0,右边界设为一个足够大的值(如 1e8)
int l = 0, r = 1e8;
while (l < r) {
LL mid = (l + r + 1) / 2;
if (calc(mid) >= k) l = mid; // 可行,尝试更大
else r = mid - 1; // 不可行,减小
}
cout << l << endl;
return 0;
}
2. 砍树
题目描述 有 $n$ 棵树,高度为 $a_i$。伐木机设定高度为 $H$,高于 $H$ 的部分被锯下。要求锯下的木材总量至少为 $m$。求 $H$ 的最大值。
解题思路 设最终结果为 $ret$。
- 当 $H \le ret$ 时,得到的木材量 $C \ge m$(高度越低,锯得越多)。
- 当 $H > ret$ 时,得到的木材量 $C < m$(高度越高,锯得越少)。 同样具备二段性,可以通过二分高度 $H$ 来求解。
代码实现
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
LL a[N];
int n;
LL m;
// 计算设定高度 mid 时能得到的木材总量
LL calc(LL mid) {
LL ret = 0;
for (int i = 1; i <= n; i++) {
if (a[i] - mid > 0) ret += a[i] - mid;
}
return ret;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
// 二分范围:高度从 1 到 2e9
LL l = 1, r = 2e9;
while (l < r) {
LL mid = (l + r + 1) / 2;
if (calc(mid) >= m) l = mid; // 满足需求,尝试更高
else r = mid - 1; // 不满足,降低高度
}
cout << l << endl;
return 0;
}
三、总结
二分答案的关键在于识别解空间的单调性。一旦确认了「答案变大,判定结果变好/变坏」的规律,就可以套用模板。这两道题虽然场景不同,但核心逻辑一致:通过二分缩小搜索范围,用线性扫描验证可行性。掌握这一思维,面对类似的最优化问题时就能迎刃而解。


