二分只有五行代码,但它是唯一一个”人人都会写、人人都写错”的算法。本讲的组织方式因此和别的讲不同:不是先讲原理再讲应用,而是先把”为什么会写错”讲透,再讲它真正值钱的那一半——二分答案。
一句话主线:
二分查找回答”这个值在哪”;二分答案回答”当我不知道答案是多少、但给我一个答案我能验证它对不对”。后者才是竞赛里的主力。
P2440 木材加工:有 $n$ 根木头,长度分别为 $L_1,\dots,L_n$。要把它们切成 $K$ 段等长的小段(可以有剩料),求这个长度最大能是多少。$n\le10^5$,$L_i\le10^8$。
正着想很难:长度取多少?没有公式。
但反着想很容易:给你一个长度 $L$,你能不能判断”切得出 $K$ 段吗”? 能——每根木头能切出 $\lfloor L_i/L\rfloor$ 段,加起来和 $K$ 比一下就行,$O(n)$。
而且这个判定有个关键性质:$L$ 越小越容易切够。于是”能不能切出 $K$ 段”这件事,随着 $L$ 从 1 增大到 $\max L_i$,是真真真……假假假——它单调。
单调的判定 + 有界的答案 = 二分。 这就是本讲下半部分的全部内容。
那为什么不直接从大到小一个个试? 实测($n=10^5$、答案范围 $10^8$):
| 做法 | 判定函数调用次数 | 用时 |
|---|---|---|
| 二分答案 | 26 次 | 0.0072 s |
| 逐个枚举答案 | 6690 万次 | 折算约 18375 秒(跑 300 次实测 0.0824 s) |
快约 250 万倍。 而 26 次这个数字不是巧合——$\log_2(10^8)=26.6$。二分的全部收益就是把”试 $N$ 次”压成”试 $\log_2 N$ 次”。
但在讲二分答案之前,得先把二分本身写对。 这是下一节的事,而它比想象中难。
二分的逻辑只有一句:“看中间,扔掉一半”。写错的原因全部集中在一件事上:
“扔掉一半”到底扔掉了哪些?中间那个元素被扔了没有?
这件事由三个约定共同决定,三者必须成套,换一个就得全换:
| 约定 | 选择 A | 选择 B |
|---|---|---|
| 区间形态 | 闭区间 $[l,r]$ | 左闭右开 $[l,r)$ |
| 循环条件 | while (l <= r) |
while (l < r) |
| 收缩方式 | l = mid+1 / r = mid-1 |
r = mid / l = mid+1 |