课程 · C++ 速查 · STL 专题 · 第 5 块

lower_bound 和 upper_bound:在排好序的数组上找

学会用这两个函数在一个排好序的数组里找位置,以及数一个区间里有多少个数。

第 5 块(共 12 块已写好)8 条动作3 处陷阱2 道例题2 道练习4 份可运行代码

读这一页之前,先知道读完要能做什么

  • 说清lower_bound 找的是「第一个 ≥ x」,upper_bound 找的是「第一个 > x」
  • 写出来「[L, R] 里有多少个数」—— 就是一个减法
  • 不犯两个错:忘了先 sort、忘了减 v.begin()

这是什么

5 段

一句话:在一个已经排好序的数组里,用 O(log n) 找到某个数的分界位置。

★★ 先把「第一个」钉死 —— 这两句是整块内容的全部:

  lower_bound(v, x) = 第一个 ≥ x 的位置

  upper_bound(v, x) = 第一个 > x 的位置

★ 注意这两句里都没有「x 在不在」。x 不在数组里时它们照样返回一个位置,那个位置是「第一个比它大的元素」—— 找不到就是 v.end()。

完整动作表

8 条

复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。

写法干什么代价
lower_bound(v.begin(), v.end(), x) - v.begin()第一个 ≥ x 的下标
★★ 一定要减 v.begin() 才拿到下标;⚠⚠ 前提是 v 已排序
O(log n)
upper_bound(v.begin(), v.end(), x) - v.begin()第一个 > x 的下标
和 lower_bound 只差一个等号
O(log n)
upper_bound(v, R) - lower_bound(v, L)[L, R] 里有多少个数(两端都含)
★★ OI 里最常问的一问,答案就是这个减法
O(log n)
lower_bound(v, R) - lower_bound(v, L)[L, R) 里有多少个(左闭右开)
和上面差的就是「刚好等于 R 的那几个」
O(log n)
upper_bound(v, x) - v.begin()≤ x 的有多少个
★ 从 v.begin() 数到 upper_bound 为止
O(log n)
binary_search(v.begin(), v.end(), x)x 在不在 —— 只给 true/false
⚠ 它不给位置 —— 别拿它的结果当下标用
O(log n)
v.erase(lower_bound(v, x), upper_bound(v, x))删掉所有等于 x 的
⚠⚠ 两个迭代器必须来自同一个容器(见下面的陷阱)
O(n)
v.insert(lower_bound(v, x), y)在保持有序的前提下插入 y
⚠ 「找到位置」是 O(log n),但「插进去」要搬元素,是 O(n)
O(n)

⭐ 这一节最容易错的地方(3 处)

这一节排在例题前面是故意的:读到一个坑,再去看正确的例子,记得住。 下面每一条都是「编译得过去、答案可能是错的」那种 —— 它们才是 OI 里真正扣分的地方。

① 忘了减 `v.begin()` —— 返回的是位置,不是下标

两个函数返回的都是迭代器(一个「指向某处的东西」),而你要填进答案里的是下标。 ★ 漏了减 v.begin(),你拿到的是一串内存偏移量。它不会越界、不会崩,只是答案全错。
这样写
vector<int> v = {1, 3, 3, 3, 5, 8};
int a = lower_bound(v.begin(), v.end(), 3) - v.begin();  // 对:1
int b = upper_bound(v.begin(), v.end(), 3) - v.begin();  // 对:4
int c = lower_bound(v.begin(), v.end(), 3);                  // ★ 这是个位置,不是 1
改成
// vector
int i = lower_bound(v.begin(), v.end(), x) - v.begin();
// 裸数组
int j = lower_bound(a, a + n, x) - a;
★★ 凡是这两个函数的结果要当下标用,后面必须跟一个减法。而且 —— 迭代器压根不能 cout 出来(编译器会把 ostream 的 80 个重载全列给你看),所以这个减法不是「忘了转换」的小事。

② 忘了先 `sort` —— 它不报错,直接给错答案

二分每一步都在问「往哪边缩」,而这个问题只有在序列有序时才有唯一答案。序列没排序时它照样一路二分下去,返回一个语法合法但语义错误的位置。 ★ 而这一条看不出来:数字很合理,不是 0、不是负数,只是大了一点点。(这正是下面例题 ② 跑出来的东西:正确答案 7,没排序会算成 8。)
改成
vector<int> v = readUnsorted();
sort(v.begin(), v.end());                          // ← 少了这一行,下面全是错的
int cnt = upper_bound(v.begin(), v.end(), R) - lower_bound(v.begin(), v.end(), L);
★ 调试时先 if (!is_sorted(v.begin(), v.end())) 查一下。

③ `erase` 的两个迭代器必须来自同一个容器

⚠⚠ 这条是写这一页的例题时真踩出来的:lower_bound(v.begin(), ...) 拿到的是 v 的迭代器,却拿它去 erase v 的副本 a —— 一个容器的迭代器在另一个容器上没有意义。 它编译得过、-Wall 一个警告都不报,然后运行时直接崩。
这样写
vector<int> v = {1, 3, 3, 5};
vector<int> a = v;
auto lb = lower_bound(v.begin(), v.end(), 3);   // ← 指向 v 里面
a.erase(lb, upper_bound(v.begin(), v.end(), 3));  // ★ 在 a 上 erase v 的区间
这一行的真实输出(实测,当场崩)
munmap_chunk(): invalid pointer
改成
// 要么直接在 v 上删:
v.erase(lower_bound(v.begin(), v.end(), 3), upper_bound(v.begin(), v.end(), 3));
// 要么在 a 上重新问一遍:
a.erase(lower_bound(a.begin(), a.end(), 3), upper_bound(a.begin(), a.end(), 3));
★ 记住:迭代器是「某个容器里的某个位置」,搬走了就不作数。

本书哪一章真的用到它

2 处

⚠ 下表里的文件路径和那串 probe 是逐字验过的(check:docs 第 ⑳ 条)—— 改了那份代码这儿会红,正是要的。点章号直接去看那一页。

章书里的哪一份代码用它来干什么
第 8 章code/08-binary-search/stl.cpp★★ 第 8 章专门有一份 stl.cpp,两个函数的签名、「必须先排序」、「对 set/map 别用」都在那一份里
第 22 章code/22-lis/stl.cpp★ LIS 的 O(n log n) 写法全靠它;那一章把手写版和 STL 版并排放着

例题(2 道)

每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。

例题 ① 两个函数分别在找什么

bb-find.cpp

给一个已经升序的数组和一个 x,分别求「第一个 ≥ x 的下标」和「第一个 > x 的下标」,再看两者之间夹着几个数。

★ 这一题在给两个函数定性。OI 里 lower_bound 的错误用法,十有八九是从「它就是查 x 在不在」这个错的直觉开始的。

输入格式第一行 n。第二行 n 个升序整数。第三行 x。
输出格式两个函数各自的下标和那里是什么,以及两者之差。

输入

7
1 3 3 3 5 8 9
4

输出

v = 1 3 3 3 5 8 9    找 x = 4

lower_bound(v, 4) = 下标 4   (第一个 >= 4)
   那里是 5

upper_bound(v, 4) = 下标 4   (第一个 >  4)
   那里是 5

所以 [4, 4] 里有 0 个数   ← 就是「等于 4 的有几个」
先想一想

★ 一句话钉死:lower_bound 是「第一个 ≥ x」,upper_bound 是「第一个 > x」。⚠ 注意这两句里都没有「x 在不在」 —— 找不到时它返回 v.end(),那是合法答案。

bb-find.cpp参考代码
// STL 专题 · 二分三件套 —— lower_bound 和 upper_bound
//
// ★ 一句话钉死:
// lower_bound(v, x) = 第一个 >= x 的位置
// upper_bound(v, x) = 第一个 > x 的位置
//
// ⚠⚠ 三件事必须同时知道,缺一件就会写错:
// ① **前提**:v 已经从小到大排好 —— 这是正确性问题,不是性能问题
// ② **返回的是位置不是下标** —— 当下标用要再减 v.begin()
// ③ 找不到时返回 v.end(),那是**合法答案**不是错误
//
// ⚠ 输入第一行 n,第二行 n 个**升序**整数,第三行 x。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, x;
cin >> n;
vector<int> v(n);
for (int &t : v) cin >> t;
cin >> x;
cout << "v = ";
for (int t : v) cout << t << ' ';
cout << " 找 x = " << x << "\n\n";
// ── lower_bound:第一个 >= x 的位置 ────────────────────────────
// ⚠ 减 v.begin() 才是下标。漏了它得到的是一个内存偏移量,不会崩,只是答案全错。
int lo = lower_bound(v.begin(), v.end(), x) - v.begin();
// ── upper_bound:第一个 > x 的位置 ────────────────────────────
int hi = upper_bound(v.begin(), v.end(), x) - v.begin();
cout << "lower_bound(v, " << x << ") = 下标 " << lo << " (第一个 >= " << x << ")\n";
if (lo == n) cout << " 它等于 v.end():没有比 " << x << " 大的了 —— 这是合法答案\n";
else cout << " 那里是 " << v[lo] << '\n';
cout << "\nupper_bound(v, " << x << ") = 下标 " << hi << " (第一个 > " << x << ")\n";
if (hi == n) cout << " 它也等于 v.end()\n";
else cout << " 那里是 " << v[hi] << '\n';
// ★★ 两个函数的差就是一个区间里有多少个数 —— 这是最常考的一问。
cout << "\n所以 [" << x << ", " << x << "] 里有 " << hi - lo << " 个数"
<< " ← 就是「等于 " << x << " 的有几个」\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 减 v.begin() 拿下标:int lo = lower_bound(...) - v.begin(); —— 这一步是必须的。
  2. ⚠ 判断一下是不是 v.end()(也就是下标等于 n)。是的话说明「没有比 x 大的了」,这时候不能去取 v[lo]。
  3. upper_bound 同理,只是找的是「> x」。
  4. ★ 两者之差就是「等于 x 的有几个」 —— 这道题里如果 x 在数组里,那个差就是它出现的次数。
记住这一句

★★ 拿到结果先判 == v.end(),再减 v.begin() 拿下标。这两句是这一块 90% 的用法。

用到的写法:lower_bound(v.begin(), v.end(), x) - v.begin() upper_bound(v.begin(), v.end(), x) - v.begin()

例题 ② 数一数区间里有多少个数

bb-count.cpp

给一个已经升序的数组和区间 [L, R],数一数有多少个数落在里面(两端都含)。

★ 这是 OI 里出现频率最高的一问,而答案就是两个函数相减。

输入格式第一行 n L R。第二行 n 个升序整数。
输出格式[L, R] 的个数、[L, R) 的个数、以及两者的差。

输入

7 3 8
1 3 3 3 5 8 9

输出

v = 1 3 3 3 5 8 9 

[3, 8]  里有 5 个   ← 上界用 upper_bound
[3, 8)  里有 4 个   ← 上界用 lower_bound

差了 1 个 —— 那几个就是刚好等于 8 的

顺便:<= 7 的有 5 个   ← 公式是 upper_bound(7) - v.begin()
先想一想

★ 想清楚为什么是「第一个 > R」减「第一个 ≥ L」——被减掉的那一长段,正好就是 [L, R] 的宽度。

bb-count.cpp参考代码
// STL 专题 · 二分三件套 —— 数一数有多少个数落在区间里
//
// ★ OI 里最常问的一问,而答案就是两个函数相减:
//
// [L, R] 里几个 = upper_bound(R) - lower_bound(L)
// [L, R) 里几个 = lower_bound(R) - lower_bound(L)
//
// ⚠ 输入第一行 n L R,第二行 n 个**升序**整数。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, L, R;
cin >> n >> L >> R;
vector<int> v(n);
for (int &t : v) cin >> t;
cout << "v = ";
for (int t : v) cout << t << ' ';
cout << "\n\n";
// ① [L, R] 闭区间 —— 两个函数的方向不一样
int closed = upper_bound(v.begin(), v.end(), R) - lower_bound(v.begin(), v.end(), L);
// ② [L, R) 左闭右开 —— 两边都用 lower_bound
int leftClosed = lower_bound(v.begin(), v.end(), R) - lower_bound(v.begin(), v.end(), L);
cout << "[" << L << ", " << R << "] 里有 " << closed << " 个 ← 上界用 upper_bound\n";
cout << "[" << L << ", " << R << ") 里有 " << leftClosed << " 个 ← 上界用 lower_bound\n";
cout << "\n差了 " << closed - leftClosed << " 个 —— 那几个就是刚好等于 " << R << " 的\n";
// ③ 顺手:数一数 <= x 的有几个
int x = 7;
int le = upper_bound(v.begin(), v.end(), x) - v.begin();
cout << "\n顺便:<= " << x << " 的有 " << le
<< " 个 ← 公式是 upper_bound(" << x << ") - v.begin()\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 闭区间 [L, R]:upper_bound(R) - lower_bound(L)。⚠ 两个函数的方向不一样,别写成一样。
  2. 左闭右开 [L, R):lower_bound(R) - lower_bound(L) —— 两边都用 lower_bound。
  3. ★ 两者之差就是「刚好等于 R 的那几个」,可以拿它自查。
  4. ⚠ 前提仍然是 sort 过。这一页的代码假设输入已升序;没排序会算出一个只大一点点的数(比如正确答案 7 算成 8),看代码很难发现。
记住这一句

★★ [L, R] 里几个 = upper_bound(R) − lower_bound(L);[L, R) = lower_bound(R) − lower_bound(L);<= x 有几个 = upper_bound(x) − v.begin()。

用到的写法:upper_bound(v.begin(), v.end(), x) - v.begin() lower_bound(v.begin(), v.end(), x) - v.begin()

练习(2 道)

只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。

练习 ① 删掉所有等于 x 的

bb-erase.cpp

给一个已经升序的数组和一个 x,删掉其中所有等于 x 的元素,输出剩下的。

★ 把两个函数接起来用 —— 这是它们最常见的组合,也是这一块的收尾。

输入格式第一行 n x。第二行 n 个升序整数。
输出格式删完的序列和长度变化,外加刚才被删的那一段是第几个到第几个。

输入

7 3
1 3 3 3 5 8 9

输出

v = 1 3 3 3 5 8 9 
删掉所有等于 3 的

要删的是 [1, 4) 这一段,长度 3

删完: 1 5 8 9 
长度 7 -> 4
提示

★ lower_bound 给的是段头、upper_bound 给的是段尾,而 erase(first, last) 删的是左闭右开 [first, last)。⚠ 还有一个更隐蔽的:这两个迭代器必须来自同一个容器。

bb-erase.cpp参考答案
这段代码是怎么想出来的
  1. 一句搞定:v.erase(lower_bound(v.begin(), v.end(), x), upper_bound(v.begin(), v.end(), x));
  2. ★ 问在 v 上、删也在 v 上 —— 两个迭代器都来自 v。⚠ 如果你先把数组拷一份再在副本上删,这两个迭代器就失效了(见上面的陷阱 ③)。
  3. ⚠ 想在删之前知道那一段有多长,就先问一次记下来,再删。
记住这一句

★★ v.erase(lower_bound(v, x), upper_bound(v, x)) —— 两个函数的差是「等于 x 的有几个」,把那个差的那一段删掉就对了。

用到的写法:v.erase(lower_bound(v, x), upper_bound(v, x)) lower_bound(v.begin(), v.end(), x) - v.begin()

练习 ② 插入位置:lower_bound 给的就是那儿

bb-insert.cpp

给一个升序数组和一个 x,问 x 应该插在哪(插完仍然升序),以及有几个数比它小。

★ 这一题是 lower_bound 的第二个高频用法:它给的位置就是「插进去还能保持有序」的那一处。

输入格式第一行 n x。第二行 n 个升序整数。
输出格式插在哪、比它小的有几个,以及真的插进去之后的结果。

输入

7 5
1 3 3 3 5 8 9

输出

v = 1 3 3 3 5 8 9 
x = 5

插在下标 4(第 5 个)
比 5 小的有 4 个
(因为 pos 前面那 4 个都 < 5,第 5 个开始就 >= 5 了)

插完: 1 3 3 3 5 5 8 9 
长度 7 -> 8
提示

★ 想清楚 lower_bound 给的是「第一个 ≥ x 的位置」——把它插在那儿,前面全都比它小、后面全都比它大,有序不就保住了吗。⚠ 另外:有多少个数比 x 小,是不是就是那个下标?

bb-insert.cpp参考答案
这段代码是怎么想出来的
  1. pos = lower_bound(...) - v.begin(); 就是插入位置。
  2. ★ 「比 x 小的有几个」正好等于 pos —— 因为它前面那 pos 个都 < x,而从第 pos 个开始就 >= x 了。这一条很好用,很多题直接输出它。
  3. 真要插进去:v.insert(lower_bound(v.begin(), v.end(), x), x);
  4. ⚠ ⚠ 两笔账要分开:「找位置」是 O(log n),「插进去」要搬后面所有元素,是 O(n)。一个一个插进去的写法就是 O(n²)。
记住这一句

★★ lower_bound 的两个高频用法:数「< x 的有几个」,和找「插在哪」。⚠ 而插入本身是 O(n) —— 反复插会变 O(n²)。

用到的写法:lower_bound(v.begin(), v.end(), x) - v.begin() v.insert(lower_bound(v, x), y)

做完了去哪儿

← 上一块:sort:排序和自定义比较器 · 回 STL 专题目录 · 下一块:queue / stack / deque:一头动还是两头动 →

⚠ 本页说的是 STL 怎么用。算法本身(Dijkstra、单调栈、状压……)在53 章正课里,而每一章末尾都会告诉你它用到了哪些 STL。