读这一页之前,先知道读完要能做什么
- 写出来去重三句:
s.insert(x)、s.count(x)查在不在、遍历出来从小到大 - 知道重复的
insert什么都不做、也不报错 - 用得上成员版
lower_bound:s.lower_bound(x),不是lower_bound(s.begin(), …)
这是什么
一句话:set 是「自动去重、自动排好序的一袋东西」。set<int> s; 之后 s.insert(x); —— 重复的插不进去。
★ set 只回答「有没有」;要数「有几个」请用第 8 块的 map 计数。
★★ set 和第 8 块的 map 一样有序、查找是 O(log n),自带的 lower_bound 是成员函数,写 s.lower_bound(x)。
完整动作表
复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。
| 写法 | 干什么 | 代价 |
|---|---|---|
set<T> s; | 开一个集合(set<int>、set<string> …)★ 装结构体也行,只要那种类型能比大小 | O(1) |
s.insert(x) | 放进去;已经有了就什么都不做 ★★★ 自动去重就靠这一句,不用先 sort | O(log n) |
s.count(x) | x 在不在(0 或 1) ★★ 查存在性的标准写法,等价于 find(x) != s.end() | O(log n) |
s.erase(x) | 按值删掉 x ⚠ 和 vector 的 erase(按位置删、给区间)不是一回事 | O(log n) |
for (int x : s) | 遍历 ★★★ 出来从小到大 —— 「去重 + 排序」两句搞定 | O(n) |
s.lower_bound(x) | 第一个 ≥ x 的位置 ★ 它是成员函数: s.lower_bound(x),不是 lower_bound(s.begin(), …) | O(log n) |
s.size() / s.empty() | 里面几个 / 空了吗 ⚠ 和 int 比大小写 (int)s.size() | O(1) |
⭐ 这一节最容易错的地方(1 处)
这一节排在例题前面是故意的:读到一个坑,再去看正确的例子,记得住。 下面每一条都是「编译得过去、答案可能是错的」那种 —— 它们才是 OI 里真正扣分的地方。
① `insert` 重复了什么都不做 —— set 不许有两个一样的
s.insert(x) 撞上已经存在的 x 时什么都不做(也不报错)。所以「插两次」不会有两个,s.size() 还是 1。
★ 于是你以为在计数,其实早把重复的丢了 —— 答案比真值小,而且看不出是哪一步错的。set<int> s; s.insert(5); s.insert(5); // s.size() == 1
map<int,int> cnt; cnt[5]++; cnt[5]++; // cnt[5] == 2本书哪一章真的用到它
⚠ 下表里的文件路径和那串 probe 是逐字验过的(check:docs 第 ⑳ 条)—— 改了那份代码这儿会红,正是要的。点章号直接去看那一页。
| 章 | 书里的哪一份代码 | 用它来干什么 |
|---|---|---|
| 第 3 章 | code/03-recursion-tree/p1157Set.cpp | ★★ 第 3 章就把组合扔进 set —— 自动去重,而且出来天然是字典序 |
| 第 36 章 | code/36-dsu/p1892.cpp | ★ 每个小孩的「敌人集合」就是它 |
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。
例题 ① 去重 + 从小到大:一句话的事
给一串整数(有重复、无序),用 set 把它变成「不重复、从小到大」,再回答两件事:某个 x 在不在、第一个 ≥ x 的是谁。
★ 对照第 4 块那道去重题:那边是 sort → unique → erase 三句,这边 insert 一句就完了。
n。第二行 n 个整数。第三行一个整数 x。x 在不在、第一个 ≥ x 的值和它是第几个。输入
10 5 3 9 1 5 3 9 7 1 7 5
输出
读进 10 个,set 里 5 个: 1 3 5 7 9 5 在不在?在 ← count 只查不建 第一个 >= 5 的是 5,下标(第几个)2
x = 5 在里面,而且它就是「第一个 ≥ 5」,排在第 2 个(从 0 数)。★ 想清楚三件事:① 重复的怎么处理;② 出来的顺序是谁给的;③ 「第几个」怎么数 —— set 的迭代器只能 ++,不能 + 2,要用 distance。
// STL 专题 · set —— 一句话:去重 + 从小到大//// ★ set 是个「不许重复、自动排好序」的袋子:// insert(x) 放进去(已经有了就什么都不做 ⇒ 自动去重)// count(x) x 在不在(0 或 1)// 遍历 出来就是从小到大//// ⚠ 它是**有序**的,所以能用成员 lower_bound —— 和第 3 块那个函数一个意思。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; set<int> s; for (int i = 0; i < n; i++) { int x; cin >> x; s.insert(x); // ★ 重复的插不进去,不用先 sort }
cout << "读进 " << n << " 个,set 里 " << s.size() << " 个:"; for (int x : s) cout << ' ' << x; // ★ 遍历出来就是从小到大 cout << "\n\n";
int x; cin >> x; cout << x << " 在不在?" << (s.count(x) ? "在" : "不在") << " ← count 只查不建\n";
auto it = s.lower_bound(x); // ★ 第一个 >= x 的位置 if (it == s.end()) cout << "第一个 >= " << x << " 的:没有(到末尾了)\n"; else cout << "第一个 >= " << x << " 的是 " << *it << ",下标(第几个)" << distance(s.begin(), it) << "\n"; return 0;}点「运行 ▶」看结果
- 读一个插一个:
s.insert(x);—— 重复的插不进去,不用先 sort。 - ★ 遍历就是有序的:
for (int x : s)出来从小到大 —— 「去重 + 排序」两句搞定。 - 查在不在:
s.count(x)—— 0 或 1,而且不会像m[k]那样顺手建键。 - 第一个 ≥ x:
auto it = s.lower_bound(x);—— 它是成员函数,写s.lower_bound(x);⚠ 拿到先判it == s.end()(「没有」也是合法答案),数第几个用distance(s.begin(), it)。
★★ set 把「去重、有序、查在不在」三件事合成了一件事。键是字符串、是 pair 都行 —— 只要那种类型能比大小。
用到的写法:set<T> s; s.insert(x) s.count(x) for (int x : s) s.lower_bound(x)
例题 ② 一堆单词去重排好,再回答「在不在」
给 n 个单词(可能重复),输出去重后按字典序的,并对 m 个查询各回答一遍「在不在」。
n。之后 n 行单词。然后 m。之后 m 行查询。yes / no。输入
6 apple banana apple cherry banana fig 3 banana cherry date
输出
apple banana cherry fig yes yes no
banana 出现两次只留一个;date 没出现过,查询输出 no。★ insert 一句去重,遍历出来就是字典序 —— set<string> 自带 <。⚠ 问在不在用 count,别想着用下标。
// STL 专题 · set 例题 —— 单词去重排好,回答「在不在」//// ★ set<string> 自动去重、遍历出来自动按字典序(字符串自带 <)。// insert 一句顶掉 sort + unique;count 只查不建。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; set<string> s; for (int i = 0; i < n; i++) { string w; cin >> w; s.insert(w); // ★ 重复的插不进去,不用先 sort }
for (auto w : s) cout << w << '\n'; // ★ 出来就是字典序
int m; cin >> m; for (int i = 0; i < m; i++) { string q; cin >> q; cout << (s.count(q) ? "yes" : "no") << '\n'; } return 0;}点「运行 ▶」看结果
- 读一个插一个:
s.insert(w);—— 重复的自动丢掉,不用先 sort。 - 遍历:
for (auto w : s)—— 出来从小到大,字符串就是字典序。 - 查询:
cout << (s.count(q) ? "yes" : "no");——count只查不建。
★★ set<string> 也能当「有序的去重袋」—— 键换成字符串就行了。
用到的写法:set<string> s; s.insert(x) for (auto x : s) s.count(x)
练习(2 道)
只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。
练习 ① 两个集合的交集和差集
给两串整数,输出「两边都有」的、只在 A 里的、只在 B 里的,都按从小到大。
★ 这是 set 最顺手的一类题:去重排序它自己管,你只要拿着一边去问另一边。
n m。第二行 n 个整数(A)。第三行 m 个整数(B)。输入
6 5 4 1 7 2 9 4 3 7 2 15 4
输出
A(去重后):5 个: 1 2 4 7 9 B(去重后):5 个: 2 3 4 7 15 交集 A ∩ B: 2 4 7 共 3 个 只在 A 里: 1 9 共 2 个 只在 B 里: 3 15 共 2 个
★ 想清楚拿什么去查什么:for (int x : a) 里面问 b.count(x)。⚠ 别拿 a 的迭代器去 b 上比 —— 迭代器是「某个容器里的位置」,搬不出去。
★★ 「从 A 遍历,去 B 里问 count」就是集合运算的固定套路。反过来问也行,但两边都要遍历的话就多扫一趟。
用到的写法:set<T> s; s.insert(x) s.count(x) for (int x : s) s.size()
练习 ② 两个集合的并集和「只在其中一个」
给两串整数,输出 A∪B(都放进去)和「只在 A 或只在 B」的,都去重、从小到大。
n m。第二行 n 个整数(A)。第三行 m 个整数(B)。输入
6 5 1 3 5 7 9 3 3 5 11 7 7
输出
并集: 1 3 5 7 9 11 (6 个) 只在其中一个: 1 9 11 (3 个)
★ 「只在其中一个里」= a.count(x) ^ b.count(x)。⚠ ^ 是「或者但不都」;并集最省事的写法是把两边都 insert 进同一个 set,遍历它自带去重排序。
★★ set 的集合运算都是「遍历一边,去另一边用 count 问一下」。
← 上一块:map:键 → 值的一张表 · 回 STL 专题目录 · 下一块:bitset / array / pair:三个顺手的小件 →
⚠ 本页说的是 STL 怎么用。算法本身(Dijkstra、单调栈、状压……)在53 章正课里,而每一章末尾都会告诉你它用到了哪些 STL。