# `set` 包含不同元素的有序集合容器,集合内的元素不重复。使用比较操作,底层是红黑树(平衡树)。 头文件:`<set>` 定义:`set<类型, 比较器> 容器名`,可以用初始化列表或另一个 `set` 初始化。不指定比较器时使用默认的 `<` 比较规则进行排序。 `set` 的模板参数为 `set<typename, compare>`(实际上还有一个模板参数)。 `set<int> s, t = {1, 2, ..., M}, a = t`,时间复杂度为 $O(1), O(M \log M), O(M)$。 ## 基本操作 迭代器相关内容请参考 [迭代器](迭代器.md)。 令 $N$ 为当前容器 `s` 的大小,$x$ 为元素,`it` 为迭代器。 下面的复杂度记号指代时间复杂度。 ### 修改 - `s.clear()` 清空,$O(N)$ - `s.insert()` 插入元素 - `s.insert(x)`,$O(\log N)$ - 如果插入之前容器内存在元素 `x`,则不插入 - `s.insert({1, 2, ..., M})`,$O(M \log (M + N))$ - `s.erase()` 删除元素,$O(\log N)$ - `s.erase(x)` 删除元素 $x$,容器不包含指定元素则不删除 - `s.erase(it)` 删除 `it` 指向的元素,`it` 是指向当前集合 `s` 的迭代器 - `it` 不能为尾迭代器 - 调用 `s.erase(it)` 后,迭代器 `it` 立即失效,不能再对该迭代器进行使用和操作 - `s.swap(other), swap(s, other)`,将容器和 $other$ 交换,$O(1)$ - `swap` 数组的时间复杂度为 $O(N)$ - 要求两个容器的模板参数一致 - `=` - `s = other`,用容器赋值,$O(|N| + |other|)$ - `s = {1, 2, ..., M}`,用初始化列表赋值,$O((N + M) \log (N + M))$ ```cpp std::set<int> s = {10, 20, 30, 40, 50}; auto it = s.find(30); s.erase(it); // 以下所有操作都是未定义行为: // std::cout << *it << std::endl; // 错误!解引用失效迭代器 // ++it; // 错误!使用失效迭代器 // if (it == s.end()) {} // 错误!比较失效迭代器 ``` ### 查找 - `s.count(x)`,返回容器内 $x$ 的数量,$O(\log N)$ - `s.find(x)`,返回容器内指向值为 $x$ 的元素的迭代器,如果不存在则返回 `.end()`,$O(\log N)$ - `s.lower_bound(x)` 和 `s.upper_bound(x)`,返回容器内指向大于等于和大于 $x$ 的第一个元素的迭代器,如果不存在则返回 `s.end()`,$O(\log N)$。 - 不是二分,需要跟 `lower_bound(), upper_bound()` 区分 - 直接用 `set<int> s; lower_bound(s.begin(), s.end(), x)` 时间复杂度线性 $O(N)$ - `s.empty()` 和 `s.size()`,时间 $O(1)$ - `s.begin()` 和 `s.end()`,时间 $O(1)$ ### 比较 定义 `set<typename> lhs, rhs`。 对于 `==, !=`,当 `lhs, rhs` 大小不同时,`lhs == rhs, lhs != rhs` 的比较次数为 $O(1)$(因为只需要判断大小),其余情况对两个容器内所有元素排序后进行字典序比较,时间 $O(N)$。 对于 `<, <=, >, >=`,使用字典序比较,时间 $O(N)$。 ## 比较器 请参考 [自定义比较器](自定义比较器.md)。 语法结构同 `priority_queue`,但是不用像 `priority_queue` 的比较器反过来写比较规则。可以使用比较器函数对象,可以重载运算符。 ```cpp #include <iostream> #include <set> #include <queue> #include <vector> #include <functional> using namespace std; struct Node { int a, b; bool operator<(const Node &i) const { return a > i.a; } bool operator>(const Node &i) const { return b > i.b; } }; set<Node, greater<Node>> s; set<Node, less<Node>> ss; int main() { s = {{10, 1}, {5, 2}, {1, 100}}; ss = {{10, 1}, {5, 2}, {1, 100}}; for (auto x : s) { cout << x.a << ' ' << x.b << '\n'; } cout << '\n'; for (auto x : ss) { cout << x.a << ' ' << x.b << '\n'; } return 0; } /* output: 1 100 5 2 10 1 10 1 5 2 1 100 */ ``` ### `set` 上比较器的细节 令比较器为 `comp(a, b)`。 `set` 不直接使用 `==` 运算符,而是通过比较器(`comp`)判断两元素的相等关系。 `set` 中判断两元素 $a, b$ 相等的条件为 `!comp(a, b) && !comp(b, a)`(联想比较器什么时候返回真)。 当使用 `set` 存储内容不同但关键字相同的元素时,会有如下场景。 ```cpp struct Node { int a, id; // 值,编号 }; struct cmp { bool operator()(const Node &i, const Node &j) const { return i.a < j.a; } } f; int main() { set<Node, cmp> s; s.insert({1, 3}), s.insert({1, 5}), s.insert({-1, -1}); for (Node x : s) { cout << x.a << ' ' << x.id << '\n'; } return 0; } /* output: -1 -1 1 3 */ ``` 如果对于内容不同但关键字相同的元素也需要存储,则比较器应为: ```cpp struct Node { int a, id; }; struct cmp { bool operator()(const Node &i, const Node &j) const { return i.a < j.a || i.a == j.a && i.id < j.id; } }; int main() { set<Node, cmp> s; s.insert({1, 3}), s.insert({1, 5}), s.insert({-1, -1}); for (Node x : s) { cout << x.a << ' ' << x.id << '\n'; } return 0; } /* output: -1 -1 1 3 1 5 */ ``` ## 基本使用 ```cpp #include <set> using namespace std; set<int> s; s.insert(3); s.insert(2); s.insert(5); cout << s.count(3) << "\n"; // 1 cout << s.count(4) << "\n"; // 0 s.erase(3); s.insert(4); cout << s.count(3) << "\n"; // 0 cout << s.count(4) << "\n"; // 1 ``` ### 比较器相关 ```cpp struct Node { int x, id; bool operator <(const Node &i) const { return x < i.x || x == i.x && id < i.id; } }; struct cmp { bool operator()(const Node &i, const Node &j) { return i.x > j.x || i.x == j.x && i.id < j.id; } } set<Node> s1; // 按 x 从小到大、x 相同时 id 从小到大 set<Node, cmp> s2; // 按 x 从大到小、x 相同时 id 从小到大 ``` ```cpp struct Node { int x, id; }; struct comp { bool operator ()(const Node &i, const Node &j) const { return i.x < j.x; // 如果 id 不同,应为 return i.x < j.x || i.x == j.x && i.id < j.id; } }; set<Node, comp> s; s.insert({1, 2}); s.insert({1, 3}); // 两元素视为相等 cout << s.size(); // 1 ``` ### 遍历 ```cpp set<int> s = {2,5,6,8}; cout << s.size() << "\n"; // 4 for (int x : s) { cout << x << "\n"; } for (auto it = s.begin(); it != s.end(); it++) { cout << *it << '\n'; } ``` 不要在遍历时执行 `.insert`、`.erase` 等修改容器的操作,否则 `RE`、`WA`。 如果需要对容器进行修改,请参考[这里](map.md#在遍历时进行修改和删除)。 ### lower_bound, upper_bound 相关 一定要注意迭代器的各种边界问题。 ```cpp set<int> s; s.insert(1); // [1] s.insert(14); // [1, 14] s.insert(9); // [1, 9, 14] s.insert(2); // [1, 2, 9, 14] auto it = s.lower_bound(3); // 指向值 9 cout << *prev(it); // 访问 < 3 的最大元素 2 cout << *s.upper_bound(7) << '\n'; // 9 cout << *s.upper_bound(9) << '\n'; // 14 cout << *s.lower_bound(5) << '\n'; // 9 cout << *s.lower_bound(9) << '\n'; // 9 cout << *s.begin() << '\n'; // 1 auto it = s.end(); cout << *(--it) << '\n'; // 14 s.erase(s.upper_bound(6)); // [1, 2, 14] auto it = s.find(x); if (it != s.end()) { // 找到元素 x } else { // 没找到元素 x } ``` 思考题:如何找到集合中离 $x$ 最近的元素? ```cpp auto it = s.lower_bound(x); if (it == s.begin()) { cout << *it << "\n"; } else if (it == s.end()) { it--; cout << *it << "\n"; } else { int a = *it; it--; int b = *it; if (x - b < a - x) cout << b << "\n"; else cout << a << "\n"; } // 如果一开始插入一个极小值和一个极大值,不需要做分类讨论 s.insert({-INF, INF}); auto it = s.lower_bound(x); int a = *it; it--; int b = *it; if (x - b < a - x) cout << b << "\n"; else cout << a << "\n"; ``` ## `multiset` 有序多重集合,集合内可以存储相同元素。 头文件、定义、初始化、绝大部分操作和成员与 `set` 一致,有几个特殊点需要记住。 令 $N$ 为多重集容器大小,$x$ 为容器中的一个元素,`multiset<int> s`,令 $M$ 为 $x$ 在多重集中出现的次数。 - `s.count(x)`,返回 $x$ 在多重集中的出现次数,$O(\log N + M)$ - `s.erase(x)`,删除多重集容器内**所有**值为 $x$ 的元素,$O(\log N + M)$ - 如果只想快速 $O(\log N)$ 查找是否存在元素 $x$、删除一个值为 $x$ 的元素,使用 `s.find(x) != s.end()` 和 `auto it = s.find(x); if (it != s.end()) s.erase(it)`。直接删除尾迭代器报错。 ```cpp // multiset 的 erase 操作 multiset<int> ms; ms.insert(1); // [1] ms.insert(14); // [1, 14] ms.insert(9); // [1, 9, 14] ms.insert(2); // [1, 2, 9, 14] ms.insert(9); // [1, 2, 9, 9, 14] ms.insert(9); // [1, 2, 9, 9, 9, 14] cout << ms.count(4) << '\n'; // 0 cout << ms.count(9) << '\n'; // 3 cout << ms.count(14) << '\n'; // 1 ms.erase(ms.find(9)); cout << ms.count(9) << '\n'; // 2 ms.erase(9); cout << ms.count(9) << '\n'; // 0 ``` ## 参考资料 [关联式容器](https://oi-wiki.org/lang/csl/associative-container/) [迭代器](https://oi-wiki.org/lang/csl/iterator/)