阶段 1 · 基础技巧 · 第 8 章普及组 J

二分查找:把边界一次性钉死

二分的思想三句话说完,可它是最容易写错的算法 —— 这一章只教一个模板,然后把它焊死。

例题:有序数组里查 lower / upper建议用时:100 分钟
这一章的目标只有一个

二分的思想你早就会了 —— 猜数字游戏,猜大了往小猜,猜小了往大猜。三句话说完。

但二分是竞赛里最容易写错的算法,没有之一。错法五花八门: 死循环、少算一个、多算一个、越界。而且这些错误常常能过样例, 在大数据上才发作。

所以这一章不追求「讲清楚思想」,追求的是: 让你从此以后,写二分不再靠试。

办法是只学一个模板,然后把它焊死在肌肉记忆里。

1一句话问题

给一个从小到大排好序的数组(可以有重复),q 次询问,每次给一个 x,回答三件事:

L = 第一个 >= x 的位置        (都比 x 小就输出 n+1)
U = 第一个 >  x 的位置        (都不比 x 大就输出 n+1)
C = x 出现了几次

输入

8 3
1 3 3 4 4 4 7 9
4
5
100

输出

4 7 3
7 7 0
9 9 0

第一行是 n 和 q,第二行是数组,之后 q 行每行一个 x。输出一行对一个询问。

  • x = 4:a[4] 是第一个 ≥ 4 的,所以 L = 4;a[7] = 7 是第一个 > 4 的,所以 U = 7;4 出现了 7 - 4 = 3 次。
  • x = 5:数组里没有 5,L = U = 7,出现 0 次。
  • x = 100:比谁都大,L 和 U 都落到 n + 1 = 9。
✓ 为什么一口气问这三个

因为它们是二分查找的三种最常见需求,而且有个漂亮的关系:

C = U - L。

「x 出现了几次」根本不用单独写代码 —— 两个二分一减就出来了。 这三个量在 C++ 标准库里就叫 lower_bound、upper_bound, 名字值得记住,第 7 步会用到。

2先用纸笔手算一遍

拿 1 3 3 4 4 4 7 9 找 x = 4:

位置:  1  2  3  4  5  6  7  8   (9)
值:    1  3  3  4  4  4  7  9    ∞
                ↑           ↑
                L=4         U=7

注意最右边那个虚构的位置 9(也就是 n+1)。它不存在于数组里, 但必须把它算成候选:当 x = 100 时,答案就落在那儿。

★ 先把这个想清楚,代码就不会错

我们要找的不是「等于 x 的位置」,而是「分界线」:

   小于 x 的一段          |    >= x 的一段
   1  3  3               |    4  4  4  7  9  ∞
                         ↑
                     这条线的位置就是 L

数组被 x 切成两半:左边全是「不满足」,右边全是「满足」。 二分要找的就是这条分界线。

这个视角非常重要,因为它把「找一个数」变成了「找一个分界点」—— 而分界点一定存在(哪怕在最左边或最右边),不需要特判「找不到」。 第 9 章的二分答案,用的就是这个视角。

3暴力:从头一个个看

brute.cpp暴力
// 有序数组里查找 —— 暴力:从头一个个看过去
//
// 输入:第一行 n q
// 第二行 n 个**从小到大**排好序的整数(可以有重复)
// 接下来 q 行,每行一个 x
// 输出:q 行,每行三个数 L U C
// L = 第一个 >= x 的位置(都比 x 小就输出 n+1)
// U = 第一个 > x 的位置(都不比 x 大就输出 n+1)
// C = x 在数组里出现了几次
//
// 为什么要同时输出这三个?因为它们正好是二分查找的三种常见需求,
// 而且 C = U - L 这个关系本身就很值得记住 ——
// 「出现了几次」不用另写代码,两个二分一减就出来了。
//
// 这份暴力从头扫到尾,O(n) 一次询问,总共 O(nq)。
// 完全正确,而且**它是那三个定义的最诚实的表述** —— 对拍时拿它当标准答案。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
if (!(cin >> n >> q)) return 0;
vector<long long> a(n + 1, 0);
for (int i = 1; i <= n; i++) cin >> a[i];
for (int k = 0; k < q; k++) {
long long x;
cin >> x;
int L = n + 1, U = n + 1;
for (int i = 1; i <= n; i++) if (a[i] >= x) { L = i; break; } // 第一个 >= x
for (int i = 1; i <= n; i++) if (a[i] > x) { U = i; break; } // 第一个 > x
cout << L << " " << U << " " << (U - L) << "\n";
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

O(n) 一次询问。它慢,但它是那三个定义最诚实的表述 —— 待会儿拿它当标准答案。

4实测:它有多慢

同题对比:线性扫描 vs 二分
询问次数也是 n,而且 x 特意取在数组后半段(线性扫描是从左往右的,x 越靠后它扫得越久)。跑完改成 100000 再来一次。
线性扫描
二分

本机实测:

n = q 线性扫描 O(nq) 二分 O(q log n)
20 000 0.20 秒 0.01 秒
50 000 1.32 秒 0.02 秒
100 000 5.21 秒 0.04 秒
✓ log n 到底有多小
n log₂n(大约要几轮)
1 000 10
1 000 000 20
1 000 000 000 30

数据规模从一千涨到十亿(一百万倍),二分只从 10 轮变成 30 轮。

这就是为什么「能二分就二分」—— 它几乎是免费的。

5★ 关键的一步:只学一个模板

★ 关键的一步
int l = 1, r = n + 1;              // 候选范围 [l, r],多留 n+1 那一格表示「找不到」
while (l < r) {
    int mid = l + (r - l) / 2;
    if (check(mid)) r = mid;       // mid 满足 → 答案在 [l, mid],mid 自己也算候选
    else l = mid + 1;              // mid 不满足 → 答案在 [mid+1, r],mid 被淘汰
}
// 出来时 l == r,就是答案

这一章只有这一个模板。所有变化都只是换 check:

找第一个 >= x   →   check(mid) 是  a[mid] >= x
找第一个 >  x   →   check(mid) 是  a[mid] >  x

写的时候不要背口诀,只问自己一句话:

「mid 满足条件的时候,答案还可能在 mid 右边吗?」

不可能 → r = mid(mid 得留着,它可能就是答案) 可能 → l = mid + 1(mid 没用了,扔掉)

三个细节,每一个都能让你 WA:

⚠ 1. 循环条件是 l < r,不是 l <= r

l == r 时区间里只剩一个数,它已经是答案了。再进循环就可能原地打转。

(l <= r 的写法也有正确版本,但它要配 r = mid - 1 和一个额外的 ans 变量。 两套模板混着记 = 一定写错。这一章只认上面那一个。)

⚠ 2. 两个分支不对称:一个是 mid,一个是 mid + 1

因为 mid 是下取整的。当区间只剩两个数(比如 l=3, r=4)时:

mid = 3 + (4 - 3) / 2 = 3        ← mid 等于 l

如果这时走的分支是 l = mid,那 l 还是 3,r 还是 4,区间一点都没缩小 —— 下一轮 mid 还是 3,永远循环下去。

下一步就让你亲眼看它死给你看。

⚠ 3. mid 要写 l + (r - l) / 2,别写 (l + r) / 2

l + r 在两者都接近 int 上限时会溢出成负数,然后数组访问越界。

本章的数据不会触发,但这是个免费的好习惯 —— 打字就多两个字符。

6亲眼看一次死循环

deadloop.cpp⚠ 故意写错的
点运行。它会打印几轮之后卡住,最后被判「超时」—— 那不是机器慢,是这段代码永远跑不完。
// ⚠ 这是一份**故意写错**的二分 —— 它会死循环
//
// 点「运行 ▶」跑一下,你会看到「超时,被强制结束」。
// 这不是机器慢,是这段代码永远跑不完。
//
// 错在哪:把 l = mid + 1 写成了 l = mid。
//
// 看着好像更保险(「万一 mid 就是答案呢」),实际上是致命的:
//
// 当区间只剩两个数时,比如 l = 3, r = 4:
// mid = 3 + (4 - 3) / 2 = 3 ← 下取整,mid 等于 l
// 如果这时走 else 分支(l = mid),l 还是 3,r 还是 4,
// **区间一点都没变**,下一轮算出来的 mid 还是 3……
//
// 于是它就在这里原地打转,直到被判超时。
//
// 记住这个判断方法:
// **写完二分,拿「区间只剩两个数」这种最小情况代进去走一遍。**
// 只要有一个分支让区间没缩小,就是死循环。
//
// 想验证「另一种写法」也一样:把 mid 改成上取整
// int mid = l + (r - l + 1) / 2;
// 配 l = mid 就是对的,配 r = mid 又会死循环。
// 所以 **mid 的取整方向和分支的写法必须配套**,不能混搭。
#include <bits/stdc++.h>
using namespace std;
int main() {
// 一个写死的小例子,不用输入
vector<long long> a = {0, 1, 3, 3, 5, 7}; // a[1..5],1 基
int n = 5;
long long x = 4; // 找第一个 >= 4 的位置(正确答案是 5)
cout << "在 1 3 3 5 7 里找第一个 >= 4 的位置(正确答案:5)\n";
cout << "如果这段输出停住了,就是死循环了 —— 等它超时被掐断。\n\n";
int l = 1, r = n + 1;
long long step = 0;
while (l < r) {
int mid = l + (r - l) / 2;
step++;
// 只打印前几轮 —— 后面全都一模一样,打出来只会刷屏
if (step <= 8) {
cout << "第 " << step << " 轮:l = " << l << ", r = " << r << ", mid = " << mid << "\n";
} else if (step == 9) {
cout << "……从这里开始,每一轮的 l、r、mid 都和上一轮完全相同。\n";
cout << "区间不再缩小,循环永远不会结束。等它超时吧。\n";
}
cout.flush();
if (a[mid] >= x) r = mid;
else l = mid; // ← 错!这里必须是 mid + 1
}
cout << "答案 = " << l << "\n";
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

输出会是这样:

第 1 轮:l = 1, r = 6, mid = 3
第 2 轮:l = 3, r = 6, mid = 4
第 3 轮:l = 3, r = 4, mid = 3
第 4 轮:l = 3, r = 4, mid = 3      ← 和上一轮一模一样
第 5 轮:l = 3, r = 4, mid = 3      ← 还是一模一样
...
★ 从此以后,写完二分做这一件事

拿「区间只剩两个数」代进去,手动走一轮。

只要有任何一个分支让区间没缩小,就是死循环。

这个检查花不了十秒钟,但它能挡住二分 90% 的翻车。 比赛的时候尤其值得做 —— 死循环意味着这道题 0 分, 而且你在考场上很难意识到「它不是慢,是根本停不下来」。

另一种写法:上取整配 l = mid

如果把 mid 改成上取整:

int mid = l + (r - l + 1) / 2;

那么配 l = mid / r = mid - 1 就是对的(这是「找最后一个满足条件的位置」常用的写法)。

关键是:mid 的取整方向和分支的写法必须配套,不能混搭。 记不住就只用第 5 步那一个模板 —— 它能解决这一章和第 9 章的全部问题。

7正解 + 标准库的写法

fast.cpp正解
// 有序数组里查找 —— 二分
//
// 输入输出和 brute.cpp 完全一样。一次询问只要 O(log n)。
//
// ============ 这一章只需要背这一个模板 ============
//
// int l = 1, r = n + 1; // 答案的候选范围是 [l, r],r 多留一格表示「找不到」
// while (l < r) {
// int mid = l + (r - l) / 2;
// if (check(mid)) r = mid; // mid 满足条件 → 答案在 [l, mid],mid 自己也可能是答案
// else l = mid + 1; // mid 不满足 → 答案在 [mid+1, r]
// }
// // 出来时 l == r,就是答案
//
// 三个要点,缺一个就会出事:
//
// 1. **循环条件是 l < r,不是 l <= r。** 相等时区间只剩一个数,已经是答案了,
// 再进循环就可能原地打转。
//
// 2. **两个分支必须是 r = mid 和 l = mid + 1。**
// 注意不对称:一边是 mid,另一边是 mid+1。
// 因为 mid 是**下取整**的,如果写成 l = mid,当 l 和 r 只差 1 时 mid 就等于 l,
// 区间一点都没缩小 —— 死循环。(deadloop.cpp 就是这么写的,可以去跑一下。)
//
// 3. **mid 要写成 l + (r - l) / 2,不要写 (l + r) / 2。**
// 后者在 l、r 都接近 int 上限时会溢出成负数。本题不会,但养成习惯。
//
// 剩下的全部变化,都只是换那个 check:
// 找第一个 >= x → check(mid) 是 a[mid] >= x
// 找第一个 > x → check(mid) 是 a[mid] > x
//
// **不要去记「找左边界要不要 +1」这种口诀。** 记住上面这个模板,
// 然后每次只问自己一句:「mid 满足条件时,答案还可能在 mid 右边吗?」
// 不可能 → r = mid(把 mid 留着)
// 可能 → l = mid + 1(把 mid 扔掉)
#include <bits/stdc++.h>
using namespace std;
int n;
vector<long long> a;
/** 第一个 >= x 的位置,没有就返回 n+1 */
int lowerPos(long long x) {
int l = 1, r = n + 1;
while (l < r) {
int mid = l + (r - l) / 2;
if (a[mid] >= x) r = mid; // mid 够大了,答案在 mid 或它左边
else l = mid + 1; // mid 太小,答案只能在右边
}
return l;
}
/** 第一个 > x 的位置,没有就返回 n+1 */
int upperPos(long long x) {
int l = 1, r = n + 1;
while (l < r) {
int mid = l + (r - l) / 2;
if (a[mid] > x) r = mid;
else l = mid + 1;
}
return l;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
if (!(cin >> n >> q)) return 0;
a.assign(n + 2, 0);
for (int i = 1; i <= n; i++) cin >> a[i];
for (int k = 0; k < q; k++) {
long long x;
cin >> x;
int L = lowerPos(x), U = upperPos(x);
cout << L << " " << U << " " << (U - L) << "\n";
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

两个函数长得几乎一模一样,唯一的区别是 >= 和 >。这就是模板的价值。

C++ 标准库早就把它们写好了:

stl.cppSTL 版
// 有序数组里查找 —— 直接用 STL
//
// 输入输出和 brute.cpp、fast.cpp 完全一样。
//
// C++ 标准库早就把这两个二分写好了:
// lower_bound(first, last, x) 第一个 >= x 的位置
// upper_bound(first, last, x) 第一个 > x 的位置
// 它们返回的是**迭代器**,减去起始迭代器就得到下标。
//
// 比赛里能用就用它 —— 少写十行就少十个出错的地方。
//
// 但请注意两件事:
// 1. **必须先排好序**,否则结果是未定义的(不会报错,只会给你一个错答案)。
// 2. **对 vector / 数组用没问题,对 set / map 千万别用**:
// 那些容器不是连续存储的,标准库的 lower_bound 在上面会退化成 O(n)。
// set 自己带成员函数 s.lower_bound(x),用那个。
//
// 那为什么还要手写一遍(fast.cpp)?
// 因为第 9 章的「二分答案」根本没有数组可以给 lower_bound 用 ——
// 那里二分的是「答案本身」,check 是一个自己写的函数。
// 手写模板必须刻进肌肉记忆,STL 只是顺手的时候用。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
if (!(cin >> n >> q)) return 0;
vector<long long> a(n);
for (int i = 0; i < n; i++) cin >> a[i]; // 这里用 0 基,方便和迭代器换算
for (int k = 0; k < q; k++) {
long long x;
cin >> x;
// 迭代器 - begin() 得到 0 基下标,+1 换成题目要求的 1 基
int L = (int)(lower_bound(a.begin(), a.end(), x) - a.begin()) + 1;
int U = (int)(upper_bound(a.begin(), a.end(), x) - a.begin()) + 1;
cout << L << " " << U << " " << (U - L) << "\n";
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 有 STL 了,为什么还要手写

两个理由:

  1. 第 9 章的二分答案根本没有数组给你 lower_bound。 那里二分的是「答案本身」,check 是一个自己写的函数。手写模板必须会。
  2. lower_bound 用在 set/map 上会退化成 O(n) —— 它们不是连续存储的。那两个容器要用自己的成员函数 s.lower_bound(x)。

比赛里能用 STL 就用(少写十行少十个错),但模板要能默写。

8单步看区间怎么减半

二分:每一轮砍掉一半候选
第 1 / 6 步
1
3
3
4
4
4
7
9
9
12
15
20
∞
l
r
1
2
3
4
5
6
7
8
9
10
11
12
13
还剩多少个候选
13
候选区间
[1, 13]
最右边那格虚线 ∞ 是数组外面的 n+1 —— 它是「找不到」的落脚点, 所以模板里 r 的初值必须是 n+1,不是 n。
要找「第一个 >= 4 的位置」。候选范围一开始是 [1, 13] —— 多留出 13 这一格,专门表示「整个数组都不满足」。

盯住两件事:

  • 蓝色的候选区间每一轮恰好少一半,一次都没有例外。
  • 最右边那个虚线的 ∞ 就是 n+1。把 x 改成 100 播一遍 —— 它最后收敛到那一格。这就是模板里 r 初值必须是 n+1 而不是 n 的原因。

然后切到「第一个 > x」再看一遍:整个过程只有一个符号变了,>= 变成 >。 数组里有三个 4,两种模式的落点正好把它们夹在中间。

trace.cpp过程演示
文字版的同一件事,还会告诉你实际轮数和 log2(n+1) 的理论上限。
// 二分 —— 把每一轮区间怎么收缩打印出来
//
// 输入:n q / n 个升序整数 / q 行 x(用小数据)
// 输出:每次二分的 l、mid、r,以及区间是怎么减半的
//
// 跑一遍,数一下轮数:n = 16 时最多 5 轮,n = 1000 时最多 10 轮,
// n = 10⁹ 时也只要 30 轮。这就是 log 的威力 ——
// **数据规模翻十亿倍,工作量只涨 30 倍。**
//
// 另外注意每一轮的区间长度:16 → 8 → 4 → 2 → 1,一次砍一半,一次都没有例外。
// 如果你的二分某一轮区间没变小,那就是死循环(去看 deadloop.cpp)。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, q;
if (!(cin >> n >> q)) return 0;
if (n <= 0 || n > 40) { cout << "这份是用来看过程的,请用 1 <= n <= 40\n"; return 0; }
vector<long long> a(n + 2, 0);
for (int i = 1; i <= n; i++) cin >> a[i];
cout << "数组:";
for (int i = 1; i <= n; i++) cout << a[i] << " ";
cout << "\n";
for (int k = 0; k < q; k++) {
long long x;
if (!(cin >> x)) break;
cout << "\n找第一个 >= " << x << " 的位置:\n";
int l = 1, r = n + 1, step = 0;
while (l < r) {
int mid = l + (r - l) / 2;
cout << " 第 " << ++step << " 轮:区间 [" << l << ", " << r << "](还剩 " << (r - l + 1)
<< " 个候选),mid = " << mid << ",a[" << mid << "] = "
<< (mid <= n ? to_string(a[mid]) : string("(越界,视为 +∞)"));
if (mid <= n && a[mid] >= x) {
cout << " >= " << x << " → 答案在 mid 或左边,r = mid\n";
r = mid;
} else if (mid <= n) {
cout << " < " << x << " → 答案在 mid 右边,l = mid + 1\n";
l = mid + 1;
} else {
cout << " → r = mid\n";
r = mid;
}
}
cout << " 收敛到 l = r = " << l;
if (l == n + 1) cout << "(数组里所有数都比 " << x << " 小,没有答案)\n";
else cout << ",a[" << l << "] = " << a[l] << "\n";
cout << " 一共 " << step << " 轮(n = " << n << ",理论上限约 log2(" << (n + 1) << ") ≈ "
<< (int)ceil(log2((double)(n + 1))) << " 轮)\n";
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

9★ 对拍验证

★ 正确的用法

把「二分」那一栏换成你自己默写的,再点开始。

对拍器
生成器专攻边界:数组取值只有 0~8(重复元素满地都是,lower 和 upper 的区别才看得出来)、n 会取到 1、x 一半命中一半不命中,还会出现比所有数都小 / 都大的情况。
// 有序数组里查找 —— 二分
//
// 输入输出和 brute.cpp 完全一样。一次询问只要 O(log n)。
//
// ============ 这一章只需要背这一个模板 ============
//
// int l = 1, r = n + 1; // 答案的候选范围是 [l, r],r 多留一格表示「找不到」
// while (l < r) {
// int mid = l + (r - l) / 2;
// if (check(mid)) r = mid; // mid 满足条件 → 答案在 [l, mid],mid 自己也可能是答案
// else l = mid + 1; // mid 不满足 → 答案在 [mid+1, r]
// }
// // 出来时 l == r,就是答案
//
// 三个要点,缺一个就会出事:
//
// 1. **循环条件是 l < r,不是 l <= r。** 相等时区间只剩一个数,已经是答案了,
// 再进循环就可能原地打转。
//
// 2. **两个分支必须是 r = mid 和 l = mid + 1。**
// 注意不对称:一边是 mid,另一边是 mid+1。
// 因为 mid 是**下取整**的,如果写成 l = mid,当 l 和 r 只差 1 时 mid 就等于 l,
// 区间一点都没缩小 —— 死循环。(deadloop.cpp 就是这么写的,可以去跑一下。)
//
// 3. **mid 要写成 l + (r - l) / 2,不要写 (l + r) / 2。**
// 后者在 l、r 都接近 int 上限时会溢出成负数。本题不会,但养成习惯。
//
// 剩下的全部变化,都只是换那个 check:
// 找第一个 >= x → check(mid) 是 a[mid] >= x
// 找第一个 > x → check(mid) 是 a[mid] > x
//
// **不要去记「找左边界要不要 +1」这种口诀。** 记住上面这个模板,
// 然后每次只问自己一句:「mid 满足条件时,答案还可能在 mid 右边吗?」
// 不可能 → r = mid(把 mid 留着)
// 可能 → l = mid + 1(把 mid 扔掉)
#include <bits/stdc++.h>
using namespace std;
int n;
vector<long long> a;
/** 第一个 >= x 的位置,没有就返回 n+1 */
int lowerPos(long long x) {
int l = 1, r = n + 1;
while (l < r) {
int mid = l + (r - l) / 2;
if (a[mid] >= x) r = mid; // mid 够大了,答案在 mid 或它左边
else l = mid + 1; // mid 太小,答案只能在右边
}
return l;
}
/** 第一个 > x 的位置,没有就返回 n+1 */
int upperPos(long long x) {
int l = 1, r = n + 1;
while (l < r) {
int mid = l + (r - l) / 2;
if (a[mid] > x) r = mid;
else l = mid + 1;
}
return l;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
if (!(cin >> n >> q)) return 0;
a.assign(n + 2, 0);
for (int i = 1; i <= n; i++) cin >> a[i];
for (int k = 0; k < q; k++) {
long long x;
cin >> x;
int L = lowerPos(x), U = upperPos(x);
cout << L << " " << U << " " << (U - L) << "\n";
}
return 0;
}
点一下即可编辑

值得故意写错的,每一个都是真实高频错误:

  • r 的初值写成 n → 「答案是 n+1」那种情况永远算不出来
  • l = mid(少了 +1) → 死循环(对拍会报「超时」)
  • while (l <= r) → 死循环
  • lowerPos 里的 >= 写成 > → 变成了 upper,重复元素时才会暴露 —— 这就是为什么生成器一定要造重复元素
  • mid = (l + r) / 2 → 本题不会炸,但记住它有溢出风险
✓ 注意这个生成器的设计

如果数组里所有元素互不相同,那么 lower 和 upper 只差 1, 把 >= 写成 > 的错误几乎抓不住。

所以生成器把取值压到只有 0~8 —— n=12 的数组里必然有大量重复。 造数据的原则(第 1 章就说过):让「错误的直觉」在你的数据上必定失败。

10二分的适用条件

★ 二分不要求「有序」,要求的是「单调」

课本上说「二分用于有序数组」,这句话太窄了。真正的条件是:

存在一个分界点,左边全部不满足,右边全部满足(或者反过来)。

不满足 不满足 不满足 | 满足 满足 满足 满足
                    ↑
                  分界点

有序数组 + 「>= x」这个条件,恰好满足上面这个形状。 但很多题里根本没有数组 —— 只要「答案越大越容易满足」,就能二分。

那就是下一章的全部内容。

11自测

自测清单0 / 8
配套练习
这一章我卡在哪(过一个月回来看,这几行比整章正文都值钱)
下一章预告

第 9 章二分答案,是二分真正的杀手锏,也是 CSP-J/S 的高频考点。

它的套路很反直觉:不会直接求答案?那就猜一个,再写个函数验证这个猜测行不行。 而「验证」通常比「求解」简单一百倍。

这一章的模板会原封不动地用上,只是 check 从「比大小」变成「一段贪心」。