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

set:去重、查在不在、从小到大

学会自动去重、自动有序的 set:增、查、删、遍历,以及什么时候该用它而不是 map。

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

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

  • 写出来去重三句:s.insert(x)、s.count(x) 查在不在、遍历出来从小到大
  • 知道重复的 insert 什么都不做、也不报错
  • 用得上成员版 lower_bound:s.lower_bound(x),不是 lower_bound(s.begin(), …)

这是什么

3 段

一句话:set 是「自动去重、自动排好序的一袋东西」。set<int> s; 之后 s.insert(x); —— 重复的插不进去。

★ set 只回答「有没有」;要数「有几个」请用第 8 块的 map 计数。

★★ set 和第 8 块的 map 一样有序、查找是 O(log n),自带的 lower_bound 是成员函数,写 s.lower_bound(x)。

完整动作表

7 条

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

写法干什么代价
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
★ **set 只回答「有没有」;要数「有几个」就用 map 计数(第 8 块)。**

本书哪一章真的用到它

2 处

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

章书里的哪一份代码用它来干什么
第 3 章code/03-recursion-tree/p1157Set.cpp★★ 第 3 章就把组合扔进 set —— 自动去重,而且出来天然是字典序
第 36 章code/36-dsu/p1892.cpp★ 每个小孩的「敌人集合」就是它

例题(2 道)

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

例题 ① 去重 + 从小到大:一句话的事

set-unique.cpp

给一串整数(有重复、无序),用 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
10 个数里不重复的是 1 3 5 7 9 这 5 个;x = 5 在里面,而且它就是「第一个 ≥ 5」,排在第 2 个(从 0 数)。
先想一想

★ 想清楚三件事:① 重复的怎么处理;② 出来的顺序是谁给的;③ 「第几个」怎么数 —— set 的迭代器只能 ++,不能 + 2,要用 distance。

set-unique.cpp参考代码
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 读一个插一个:s.insert(x); —— 重复的插不进去,不用先 sort。
  2. ★ 遍历就是有序的:for (int x : s) 出来从小到大 —— 「去重 + 排序」两句搞定。
  3. 查在不在:s.count(x) —— 0 或 1,而且不会像 m[k] 那样顺手建键。
  4. 第一个 ≥ 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)

例题 ② 一堆单词去重排好,再回答「在不在」

set-words.cpp

给 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,别想着用下标。

set-words.cpp参考代码
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 读一个插一个:s.insert(w); —— 重复的自动丢掉,不用先 sort。
  2. 遍历:for (auto w : s) —— 出来从小到大,字符串就是字典序。
  3. 查询:cout << (s.count(q) ? "yes" : "no"); —— count 只查不建。
记住这一句

★★ set<string> 也能当「有序的去重袋」—— 键换成字符串就行了。

用到的写法:set<string> s; s.insert(x) for (auto x : s) s.count(x)

练习(2 道)

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

练习 ① 两个集合的交集和差集

set-inter.cpp

给两串整数,输出「两边都有」的、只在 A 里的、只在 B 里的,都按从小到大。

★ 这是 set 最顺手的一类题:去重排序它自己管,你只要拿着一边去问另一边。

输入格式第一行 n m。第二行 n 个整数(A)。第三行 m 个整数(B)。
输出格式两个集合去重后的样子,交集、只在 A、只在 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 上比 —— 迭代器是「某个容器里的位置」,搬不出去。

set-inter.cpp参考答案
记住这一句

★★ 「从 A 遍历,去 B 里问 count」就是集合运算的固定套路。反过来问也行,但两边都要遍历的话就多扫一趟。

用到的写法:set<T> s; s.insert(x) s.count(x) for (int x : s) s.size()

练习 ② 两个集合的并集和「只在其中一个」

set-union.cpp

给两串整数,输出 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-union.cpp参考答案
记住这一句

★★ set 的集合运算都是「遍历一边,去另一边用 count 问一下」。

用到的写法:set<int> a; a.insert(x) a.count(x) for (int x : a)

做完了去哪儿

← 上一块:map:键 → 值的一张表 · 回 STL 专题目录 · 下一块:bitset / array / pair:三个顺手的小件 →

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