二分算法是算法竞赛中最基础也最重要的技巧之一,核心思想是每次将搜索范围缩小一半,在 O(log n) 时间内找到答案。
一、核心原理
1.1 二分的前提条件
单调性:如果答案具有单调性(满足条件的答案连续分布),就可以用二分。
1.2 三大类型对比
| 类型 | 应用场景 | 终止条件 | 核心代码特征 |
|---|---|---|---|
| 二分查找 | 在有序数组中找元素 | left <= right |
直接比较 mid 与目标值 |
| 浮点二分 | 求浮点数的精确值 | right - left >= eps |
mid = (left + right) / 2 |
| 二分答案 | 求最优解(最大/最小值) | left <= right |
配合 check() 函数验证 |
二、浮点二分:计算 √2
2.1 题目描述
计算 √2,保留 3 位小数。利用 x² 在 x > 0 时的单调性。
2.2 推导过程
| 步骤 | 区间 | 中点 | 中点² | 判断 |
|---|---|---|---|---|
| 1 | [1, 2] | 1.5 | 2.25 > 2 | 太大,缩左 |
| 2 | [1, 1.5] | 1.25 | 1.5625 < 2 | 太小,缩右 |
| 3 | [1.25, 1.5] | 1.375 | 1.890625 < 2 | 太小,缩右 |
| 4 | [1.375, 1.5] | 1.4375 | 2.066 > 2 | 太大,缩左 |
| 5 | [1.375, 1.4375] | 1.40625 | 1.9775 < 2 | 太小,缩右 |
| 6 | [1.40625, 1.4375] | 1.421875 | 2.0217 > 2 | 太大,缩左 |
| 7 | [1.40625, 1.421875] | 1.4140625 | 1.9995 < 2 | 接近目标 |
2.3 完整代码
python
left, right = 1, 2
eps = 1e-4 # 精度要求,保留3位小数需要1e-4
while right - left >= eps:
mid = (left + right) / 2
if mid * mid > 2:
right = mid # 中点太大,答案在左半区间
else:
left = mid # 中点太小,答案在右半区间
print(f"{left:.3f}") # 输出 1.414
2.4 关键细节
| 注意点 | 说明 |
|---|---|
eps 的选取 |
保留 n 位小数,eps = 1e-(n+1) |
| 浮点数比较 | 不能用 ==,用区间长度判断 |
| 循环次数 | 固定 for _ in range(100) 也可,更稳定 |
三、二分答案:巧克力切割(蓝桥杯 P99)
3.1 题目描述
有 N 块巧克力,第 i 块是 H_i × W_i 的长方形。要切出 K 块大小相同的正方形,求最大可能的边长。
输入:
N K
H1 W1
H2 W2
...
HN WN
输出:最大边长
3.2 思路分析
单调性 :边长 x 越大,能切出的块数越少。具有单调性,可以二分。
check(x) :边长为 x 时,能否切出至少 K 块?
- 每块巧克力能切:
(H // x) × (W // x)块 - 总和
>= K则合法
二分策略 :求最大值 → check(mid) 合法时,尝试更大的:left = mid + 1
3.3 完整代码
python
N, K = map(int, input().split())
chocolates = []
for _ in range(N):
h, w = map(int, input().split())
chocolates.append((h, w))
def check(x):
"""边长为x时,能否切出至少K块"""
if x == 0:
return True
cnt = 0
for h, w in chocolates:
cnt += (h // x) * (w // x)
return cnt >= K
# 二分答案:最大边长范围 [1, 100000]
left, right = 1, 100000
ans = 1
while left <= right:
mid = (left + right) // 2
if check(mid):
ans = mid # mid合法,记录答案
left = mid + 1 # 尝试更大的边长
else:
right = mid - 1 # mid太大,缩小范围
print(ans)
3.4 推演验证
假设:N=2, K=5, 巧克力=[6x5, 5x5]
check(3): (6//3)*(5//3)=2*1=2, (5//3)*(5//3)=1*1=1, 总计3 < 5 → 不合法
check(2): (6//2)*(5//2)=3*2=6, (5//2)*(5//2)=2*2=4, 总计10 >= 5 → 合法
check(3)不合法,check(2)合法 → 答案为2
3.5 复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(N × log(max(H,W))) |
二分约 log(10^5) ≈ 17 次,每次遍历N块 |
| 空间 | O(N) |
存储巧克力尺寸 |
四、二分答案:跳石头(蓝桥杯 P364)
4.1 题目描述
河道长 L,起点到终点之间有 N 块石头(不含起点终点)。至多移走 M 块石头,使得选手比赛中的最短跳跃距离尽可能大。求最短跳跃距离的最大值。
输入:
L N M
D1
D2
...
DN
D_i 表示第 i 块石头与起点的距离。
4.2 思路分析
"最大值最小化" / "最小值最大化" → 经典二分答案题型。
单调性 :假设最短跳跃距离为 x,如果 x 可行,则所有 <= x 的距离也可行?不对,应该是:如果距离 x 可行(能通过移走不超过M块石头实现),那么所有 <= x 的距离也一定可行 。所以具有单调性,二分找最大的可行 x。
check(x) :判断最短跳跃距离为 x 时,需要移走多少块石头?
- 贪心策略:从左到右遍历,如果当前石头到上一块保留石头的距离
< x,则移走当前石头 - 最后检查终点距离是否
>= x
4.3 完整代码
python
L, N, M = map(int, input().split())
stones = [int(input()) for _ in range(N)]
def check(x):
"""
判断:当最短跳跃距离为x时,移走的石头数是否不超过M
"""
now_idx = 0 # 当前所在位置(起点为0)
cnt = 0 # 移走的石头数
for i in range(N):
if stones[i] - now_idx < x:
# 距离太短,必须移走这块石头
cnt += 1
else:
# 距离足够,可以跳到这里
now_idx = stones[i]
# 最后检查:最后一块保留的石头到终点的距离
if L - now_idx < x:
if cnt < M: # 还可以移走最后一块石头
return True
return False
return cnt <= M
# 二分:最短跳跃距离范围 [1, L]
left, right = 1, L
ans = 1
while left <= right:
mid = (left + right) // 2
if check(mid):
ans = mid
left = mid + 1 # 尝试更大的距离
else:
right = mid - 1
print(ans)
4.4 关键细节
| 易错点 | 正确做法 |
|---|---|
| 忘记检查终点距离 | 循环结束后必须判断 L - now_idx < x |
| 移走最后一块石头的边界 | cnt < M 而非 cnt <= M,因为还要移走最后一块 |
| 贪心策略 | 能不移就不移,距离不够才移 |
五、二分答案:第K大元素(蓝桥杯 P3404)
5.1 题目描述
n × m 的矩形乘法表,第 i 行第 j 列的元素为 i × j。求第 k 大的元素。
输入 :n m k
输出 :第 k 大的元素
5.2 思路分析
"第K小问题" → 二分答案经典应用。
单调性 :对于数字 x,乘法表中小于等于 x 的元素个数是单调递增的。
check(x) :统计乘法表中有多少个元素 <= x
- 第
i行:元素为i×1, i×2, ..., i×m i × j <= x→j <= x // i- 第
i行贡献:min(m, x // i)个
二分策略 :找最小的 x,使得 <= x 的元素个数 >= k
5.3 完整代码
python
n, m, k = map(int, input().split())
def check(x):
"""统计乘法表中有多少个元素 <= x"""
cnt = 0
for i in range(1, n + 1):
# 第i行:i*j <= x => j <= x // i
cnt += min(m, x // i)
return cnt
# 二分:答案范围 [1, n*m]
left, right = 1, n * m
ans = 0
while left <= right:
mid = (left + right) // 2
if check(mid) >= k:
# mid可能偏大或正好,尝试更小的
ans = mid
right = mid - 1
else:
# mid太小,需要更大的数
left = mid + 1
print(ans)
5.4 推演验证
n=2, m=4, k=4
乘法表:
1 2 3 4
2 4 6 8
排序后:[1, 2, 2, 3, 4, 4, 6, 8],第4大是3
check(3):
i=1: min(4, 3//1=3) = 3
i=2: min(4, 3//2=1) = 1
cnt = 4 >= 4 → 合法,尝试更小
check(2):
i=1: min(4, 2) = 2
i=2: min(4, 1) = 1
cnt = 3 < 4 → 不合法
答案为3 ✓
5.5 复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(n × log(n×m)) |
n,m <= 5×10^5,log(2.5×10^11) ≈ 38 |
| 空间 | O(1) |
无需存储矩阵,直接计算 |
六、二分答案万能模板
python
def binary_search_answer():
# 1. 确定初始范围
left, right = 最小可能值, 最大可能值
ans = 初始值
# 2. 二分循环
while left <= right:
mid = (left + right) // 2
if check(mid): # mid满足条件
ans = mid # 记录答案
# 根据题目要求调整:
# 求最大值 → left = mid + 1
# 求最小值 → right = mid - 1
left = mid + 1 # 或 right = mid - 1
else:
# 不满足条件,反向调整
right = mid - 1 # 或 left = mid + 1
return ans
def check(x):
"""
判断x是否满足条件
根据题目具体实现
"""
pass
七、题型识别指南
| 关键词 | 算法类型 | 例题 |
|---|---|---|
| "有序数组中找元素" | 二分查找 | 基础模板 |
| "保留n位小数" | 浮点二分 | √2计算 |
| "最大值最小化" / "最小值最大化" | 二分答案 | 跳石头 |
| "第K大/小" | 二分答案 | 乘法表 |
| "能否完成" + 求最优参数 | 二分答案 | 巧克力切割 |
| "至少/至多" + 求边界 | 二分答案 | 多数二分题 |
八、常见错误与调试技巧
| 错误 | 现象 | 解决方法 |
|---|---|---|
| 死循环 | left=3, right=4, mid=3,循环不变 |
检查更新逻辑,left = mid + 1 而非 left = mid |
| 整数溢出 | left + right 超出范围 |
使用 mid = left + (right - left) // 2 |
| 边界错误 | 答案差1 | 仔细分析 check() 的等号情况 |
| 浮点精度不足 | 输出与预期差0.001 | 减小 eps,或增加循环次数 |
| 二分方向搞反 | 该往左缩却往右缩 | 画数轴,明确"合法"区间方向 |
九、学习心得
二分的本质是"利用单调性进行决策排除" 。每次判断
mid后,就能确定答案在左半还是右半区间,从而将问题规模减半。
三句话记住二分答案:
- 判单调:答案是否具有单调性?
- 写check :给定
mid,能否快速判断是否合法? - 调方向:求最大还是最小?合法时往哪边缩?
掌握二分答案后,你会发现很多看似复杂的优化问题,都能转化为"猜答案 + 验证"的套路,大幅降低思维难度。