第 47 章 二分答案——从查找值到查找可行域
🏗️ 前情回顾:第 46 章你学会了在有序数组中二分查找一个具体的值。但二分的思想远不止于此——如果把"值"扩展成"问题的答案",二分就能用来猜答案。这就是本章的主角——二分答案,信息学竞赛中的高频考点。
🎯 本章目标
学完这一章,你能:
- 理解二分答案的核心思想:猜一个答案,验证是否可行
- 掌握 check 函数的设计——这是二分答案最关键的技能
- 写出整数二分答案的标准模板
- 处理浮点二分答案(精度控制)
- 解决砍树、木材加工、跳石头等经典问题
- 看到"最大值最小"或"最小值最大"就条件反射想到二分答案
📖 故事引入
你在洗澡。水太冷了,你往热水那边拧一点;太烫了,又往冷水那边拧一点。拧来拧去,终于调到一个刚刚好的温度。
这个过程你不陌生吧?其实你就在做二分答案——在一个连续的范围内,不断调整,找一个"刚刚好"的平衡点。
再想一个问题:有一排 n 棵树,高度分别是 h₁, h₂, ..., hₙ。你需要砍下总长度至少为 M 的木材。你的电锯可以设定一个高度 H,所有高于 H 的部分都会被砍下来。你希望 H 尽可能大(少砍点树),但必须满足砍下的总长度 ≥ M。
H 能取多少?你也许想:从 0 开始试,H=0 全部砍下肯定够,H=1 再试试……但这太低效了。H 最大可以是最高树的高度(比如 10^9),一个个试肯定超时。
换个思路:如果 H 的值有一个"单调性"——H 越小,砍下的木材越多——那么你可以二分 H!猜一个 H,计算砍下的总长度,够就试试更大的 H,不够就减小 H。
这就是二分答案:把"查找一个值"变成"查找一个满足条件的临界点"。
🧱 知识讲解
47.1 二分答案的适用条件
二分答案不是万能的。问题必须满足:
- 答案具有单调性:如果某个值可行,那么比它"更好"的一侧也都可行(或都不可行)
- 存在一个临界点:一侧满住条件,另一侧不满足
- check 函数高效:给定一个候选答案,能在合理时间内判断是否可行
🎯 看到题目中有"最大值最小"或"最小值最大",99% 就是二分答案!
47.2 check 函数:二分答案的灵魂
二分答案的框架和二分查找一样,但关键不在二分本身,而在 check(mid)——判断 mid 这个答案是否可行。
bool check(int mid) {
// 根据题意,判断 mid 作为答案是否可行
// 返回 true 表示可行,false 表示不可行
}47.3 整数二分答案模板
场景一:求"最大的可行值"(最大值最大化)
int l = 最小可能值, r = 最大可能值, ans = l;
while (l <= r) {
int mid = l + (r - l) / 2;
if (check(mid)) {
ans = mid; // 记录当前可行答案
l = mid + 1; // 尝试更大的
} else {
r = mid - 1; // mid 不行,只能更小
}
}
cout << ans;场景二:求"最小的可行值"(最小值最小化)
int l = 最小可能值, r = 最大可能值, ans = r;
while (l <= r) {
int mid = l + (r - l) / 2;
if (check(mid)) {
ans = mid; // 记录当前可行答案
r = mid - 1; // 尝试更小的
} else {
l = mid + 1; // mid 不行,只能更大
}
}
cout << ans;🧠 关键区别:可行时往哪边收缩。求最大值 → 可行时
l = mid+1;求最小值 → 可行时r = mid-1。
47.4 经典题一:砍树
问题:n 棵树高度 a₁...aₙ,锯片高度 H,获得木材 = Σ max(0, aᵢ - H)。需要至少 M 米木材,求 H 最大能设多少。
int n, m;
int a[1000005];
bool check(int H) {
long long sum = 0;
for (int i = 0; i < n; i++) {
if (a[i] > H) sum += a[i] - H;
}
return sum >= m; // 砍下的木材够 m 米
}
int main() {
cin >> n >> m;
int maxH = 0;
for (int i = 0; i < n; i++) {
cin >> a[i];
maxH = max(maxH, a[i]);
}
int l = 0, r = maxH, ans = 0;
while (l <= r) {
int mid = l + (r - l) / 2;
if (check(mid)) {
ans = mid;
l = mid + 1; // 可行,试试能不能更高
} else {
r = mid - 1;
}
}
cout << ans;
return 0;
}47.5 经典题二:跳石头(最小值最大化)
问题:一条直线上有 n 块石头,距起点距离为 d₁...dₙ。要去掉 m 块石头,使得相邻石头之间的最小距离尽可能大。求这个最大化的最小距离。
思路:二分"最小距离"mid。check(mid) 判断:能否通过去掉 ≤ m 块石头,使得任意相邻石头间距 ≥ mid?
check 策略:从起点开始贪心——如果当前石头和上一块保留石头的距离 < mid,就搬走当前石头;否则保留它。统计搬走的数量,判断是否 ≤ m。
bool check(int mid) {
int removed = 0;
int last = 0; // 上一块保留石头的位置(起点为 0)
for (int i = 1; i <= n; i++) {
if (d[i] - last < mid) {
removed++; // 距离不够,搬走
} else {
last = d[i]; // 保留
}
}
// 别忘了终点 L:
if (L - last < mid) removed++;
return removed <= m;
}
// 二分求最大化的最小距离🎯 "最小值最大"→ 二分 mid → check(mid) 用贪心判断 → 可行就往大里试。
47.6 浮点二分答案
如果答案不是整数而是浮点数(比如"求满足某个精度的 x"),写法基本一样,只是边界处理不同:
double l = 0, r = 1e9;
for (int i = 0; i < 100; i++) { // 迭代 100 次,精度足够
double mid = (l + r) / 2;
if (check(mid))
l = mid; // 注意:浮点二分不需要 ±1
else
r = mid;
}
cout << fixed << setprecision(6) << l;⚠️ 浮点二分不要用
while (r - l > eps),固定迭代次数(如 60~100 次)更安全,避免精度死循环。
✋ 动手试试
试试 1:实现砍树问题。输入 n=4, m=7, 高度为 20 15 10 17。手工算一下:H 分别取 15, 16, 17, 14 时各砍下多少?用二分程序验证。
试试 2:实现跳石头问题简化版。L=25, n=5, m=2, 石头位置:2, 11, 14, 17, 21。思考:搬走哪两块能使最小间距最大?最小间距最大能到多少?
试试 3:浮点二分练习——求 sqrt(2) 的近似值(不调用 sqrt 函数)。二分搜索区间 [0, 2],check(mid) 判断 mid*mid 是否 ≤ 2。迭代 60 次,输出保留 10 位小数。
⚠️ 容易犯的错
错 1:忘了单调性就二分
❌ 问题没有单调性却硬套二分 → 答案错误
✅ 二分的 check 结果必须随 mid 单调变化
错 2:check 函数写错了逻辑
❌ return sum >= m; 和 return sum <= m; 搞反
✅ 仔细读题,明确"可行"是什么意思
错 3:二分边界设错了
❌ 砍树问题中 r = 0 或忘了设 ans 初始值
✅ l 和 r 覆盖答案的所有可能范围,ans 初始为不可行的值
错 4:浮点二分用 while(r-l > eps)
❌ while (r - l > 1e-8) → eps 太小可能死循环
✅ 固定迭代次数:for (int i = 0; i < 80; i++)
错5:check 内使用 long long 的范围
❌ 砍树问题中用 int sum 存 Σ(aᵢ-H),可能溢出
✅ 用 long long 存累加结果
📝 练习
基础题
1. 选择题
(1)二分答案适用的条件是?
A. 数据有序 B. 答案具有单调性 C. n ≤ 100 D. 必须整数
(2)"求最大化的最小值"时,check 可行后应该?
A. l = mid - 1 B. l = mid + 1 C. r = mid D. 直接返回 mid
(3)浮点二分推荐用什么方式控制终止?
A. while(l < r) B. while(r-l > 1e-12) C. 固定迭代次数 D. do-while
2. 填空题
(1)二分答案的两大要素:答案具有____、____函数高效。
(2)求最大值时,check 可行则 l = ____ + 1;求最小值时,check 可行则 r = ____ - 1。
(3)砍树问题中砍下的总木材 = Σ max(0, ____ - ____)。
提高题
3. 编程题 — 木材加工
有 n 根原木,长度分别为 L₁...Lₙ。需要切成等长的 k 段小木料。求小木料的最大长度。(每段原木可以切成若干段,不能拼接。)
check(mid):判断 ∑ floor(Lᵢ / mid) ≥ k。
4. 编程题 — 数列分段
给定 n 个正整数,分成连续的 m 段,使得每段和的最大值最小。求这个最小值。
提示:二分"每段和的最大值"mid。check(mid) 贪心分段,尽量凑满 mid 再分段,看最终段数是否 ≤ m。
挑战题
5. 编程题 — 电缆切割
有 n 条电缆,长度分别为 a₁...aₙ(浮点数,单位米),精确到厘米。需要切成 k 条等长的电缆。求能切出的最大长度(精确到厘米,即保留两位小数)。
提示:把输入 ×100 转成整数(厘米),整数二分,输出时 ÷100.0。
🧠 本章小结
二分答案 = 猜答案 + 验证
适用条件:
· 答案具有单调性
· check(mid) 能高效判断是否可行
两大模板:
求最大值 → check 可行时 l = mid+1(往大试)
求最小值 → check 可行时 r = mid-1(往小试)
check 函数是灵魂:给定 mid,判断是否可行
经典问题:
砍树(最大值) → 锯片高度 H 最大
跳石头(最小值) → 最小间距最大
木材加工 → 等长小段最大长度
浮点二分:固定迭代次数(60~100 次),不要用 while(r-l>eps)
关键信号词:"最大值最小" / "最小值最大" → 二分答案!📝 配套练习
共9题。二分答案是M7题量最大的章——从砍树模板→贪心check→浮点二分→差分check→前缀和check,check函数设计能力逐级增强。。★核心(课堂必做) ◆拓展(课后练习) ★★★挑战(选做)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | luogu-P1873 | https://hydro.ac/p/luogu-P1873 | 二分答案、砍树、check=m锯下≥M |
| ◆ 拓展 | luogu-P2440 | https://hydro.ac/p/luogu-P2440 | 二分答案、木材加工、check=段数≥k |
| ◆ 拓展 | luogu-P2678 | https://hydro.ac/p/luogu-P2678 | 二分答案+贪心check、跳石头 |
| ◆ 拓展 | luogu-P1182 | https://hydro.ac/p/luogu-P1182 | 二分答案+贪心分段、数列分段 |
| ◆ 拓展 | luogu-P1163 | https://hydro.ac/p/luogu-P1163 | 浮点二分、贷款利率 |
| ◆ 拓展 | luogu-P1577 | https://hydro.ac/p/luogu-P1577 | 浮点二分、切绳子 |
| ◆ 拓展 | luogu-P1083 | https://hydro.ac/p/luogu-P1083 | 二分答案+差分check、借教室 |
| ◆ 拓展 | luogu-P1314 | https://hydro.ac/p/luogu-P1314 | 二分答案+前缀和、质量检测 |
| ★★★ 挑战 | luogu-P4343 | https://hydro.ac/p/luogu-P4343 | 二分答案、自动刷题机 |
💡 练习建议:先完成 2 道★核心题,确保掌握本章基本方法;再完成 7 道◆拓展题,覆盖不同变式和细节;★★★挑战题建议在完成拓展题后再做,涉及多知识点综合。
配套练习
共9题。二分答案是M7题量最大的章——从砍树模板→贪心check→浮点二分→差分check→前缀和check,check函数设计能力逐级增强。★核心(课堂必做) · ◆拓展(课后练习) · ★★★挑战(选做)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | luogu-P1873 | https://hydro.ac/p/luogu-P1873 | 二分答案、砍树、check=m锯下≥M |
| ◆ 拓展 | luogu-P2440 | https://hydro.ac/p/luogu-P2440 | 二分答案、木材加工、check=段数≥k |
| ◆ 拓展 | luogu-P2678 | https://hydro.ac/p/luogu-P2678 | 二分答案+贪心check、跳石头 |
| ◆ 拓展 | luogu-P1182 | https://hydro.ac/p/luogu-P1182 | 二分答案+贪心分段、数列分段 |
| ◆ 拓展 | luogu-P1163 | https://hydro.ac/p/luogu-P1163 | 浮点二分、贷款利率 |
| ◆ 拓展 | luogu-P1577 | https://hydro.ac/p/luogu-P1577 | 浮点二分、切绳子 |
| ◆ 拓展 | luogu-P1083 | https://hydro.ac/p/luogu-P1083 | 二分答案+差分check、借教室 |
| ◆ 拓展 | luogu-P1314 | https://hydro.ac/p/luogu-P1314 | 二分答案+前缀和、质量检测 |
| ★★★ 挑战 | luogu-P4343 | https://hydro.ac/p/luogu-P4343 | 二分答案、自动刷题机 |
练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 7 道◆拓展题,覆盖不同变式和细节;★★★挑战题建议在完成拓展题后再做,涉及多知识点综合。
自查清单:
- [ ] 我理解二分答案和二分查找的区别
- [ ] 我能识别"二分答案"的信号词
- [ ] 我能设计 check 函数
- [ ] 我能写出整数二分答案的正确模板
- [ ] 我知道浮点二分要用固定迭代次数
- [ ] 我能解决砍树和木材加工这类经典问题
🚀 下章预告:二分答案让你在"可行域"中高效搜索。但有些问题没有单调性,不能二分——比如"最优装载",比如"活动选择"。这时你需要另一种智慧:每步都选当下看起来最好的。这就是第 48 章——贪心算法。准备迎接最"聪明"也最容易翻车的算法范式!🎯