关于 C++,你不得不学习的 STL
众所周知,STL 由六部分组成:容器、分配器、迭代器、算法、适配器、仿函数。对于 OIer 来说,不必深究,掌握其中一些容器和算法,做某些题时就如鱼得水。
容器(Container)
前排观影提示:大多数的容器遍历都支持 for(auto i:h) 格式。
vector
vector 是 STL 提供的一种内存连续,长度可变的动态数组。
虽说动态数组,但 vector 的底层仍是定长数组。当数组大小不足时,vector 会倍增的申请、分配更多连续的空间,而之前的空间则清理删除。
定义
vector<int> h 定义一个数据类型为 int 的动态数组 \(h\)。
vector<int> h(n, k) 定义一个数据类型为 int,长度为 \(n\),初始值为 \(k\) 动态数组 \(h\)。当 \(k\) 为 \(0\)(或默认构造方式)时,可省略。
需要头文件 #include<vector>。
函数
- 元素访问
h.begin()返回一个迭代器,指向 \(h\) 的第一个元素的位置;h.end()返回一个迭代器,指向 \(h\) 的最后一个元素的后一个位置;h[x]返回 \(h\) 下标为 \(x\) 的元素;h.front()返回 \(h\) 中的第一个数;h.back()返回 \(h\) 中的最后一个数。
- 元素个数
h.size()返回 \(h\) 中的元素个数。h.empty()检查 \(h\) 是否为空。
- 元素修改
h.clear()清空 \(h\);h.push_back(int val)在 \(h\) 的末尾加入元素 \(val\),均摊时间复杂度 \(O(1)\),最坏复杂度为 \(O(n)\)(倍增申请空间);h.pop_back()删除 \(h\) 的最后一个元素,时间复杂度 \(O(1)\);h.assign(int cnt,int val)将 \(h\) 替换为 \(cnt\) 个 \(val\) 的动态数组;h.resize(int len)将 \(h\) 的大小重置为 \(len\),该操作会保留 \(h\) 中原有的元素。
- 元素修改(不常用)
h.insert(iterator pos,int val)在 \(pos\) 位置之前插入一个元素 \(val\),时间复杂度与插入位置到 \(h.end()\) 的距离有关;h.erase(iterator pos)删除 \(pos\) 位置的元素,并返回指向下一个迭代器,时间复杂度同 \(insert\) 操作;h.erase(iterator sta, iterator end)删除位于 \([sta,end)\) 之间的元素,并返回指向下一个迭代器,时间复杂度同 \(insert\) 操作;
应用
-
邻接表存图
- 定义
const int N = 1e5 + 7; struct edge { int to, val; }; vector<edge> e[N];- 存图(有向图)
for (int i = 1; i <= m; i++) { int u, v, w; cin >> u >> v >> w; e[u].push_back({v, w}); }- 对边进行排序
for (int i = 1; i <= n; i++) sort(e[i].begin(), e[i].end(), cmp);cmp为自定义排序规则(后文会讲到),如:bool cmp(edge a, edge b) { return a.val < b.val; }- 遍历
void dfs(int now) { for (int i = 0; i < e[now].size(); i++) { auto [p, val] = e[now][i]; // ... dfs(p); // ... } } -
桶优化
dijkstra有兴趣者可以看这个,此处不再赘述。(注:此为不常用算法,感兴趣者了解即可)
while (1) { vis[now] = 1; for (int i = fir[now]; i; i = nex[i]) { int p = poi[i]; if (dis[p] > dis[now] + val[i]) { dis[p] = dis[now] + val[i]; T[dis[p]].push_back(p); maxn = max(maxn, dis[p]); } } last = now; for (int i = 1; i <= maxn; i++) { if (T[i].size()) { if (vis[T[i][0]] == 0) now = T[i][0]; last = i; T[i].erase(T[i].begin()); break; } } if (last == now) break; } -
模拟平衡树(注:该实现方式理论复杂度为 \(O(n^2)\),仅供娱乐,正解请学习相关数据结构)
cin >> op >> k; if (op == 4) cout << h[k - 1] << endl; if (op == 2) h.erase(lower_bound(h.begin(), h.end(), k)); if (op == 1) h.insert(upper_bound(h.begin(), h.end(), k), k); if (op == 6) cout << (*upper_bound(h.begin(), h.end(), k)) << endl; if (op == 5) cout << (*--lower_bound(h.begin(), h.end(), k)) << endl; if (op == 3) cout << (lower_bound(h.begin(), h.end(), k) - h.begin() + 1) << endl;(不要在意 1,2,3,4 的顺序,个人感觉这样逐行递增比较好看。)
array
与 vector 正好相反,array 是 STL 提供的一种固定大小的数组。
与 C 语言中的数组相比,具有更好的类型安全和内存管理特性。 ——菜鸟教程
定义
array<int, N>a 代表定义一个类型为 \(int\),大小为 \(N\) 的数组 \(a\)。
值得注意的是,\(N\) 必须是编译期常量。
需要头文件 #include<array>。
应用
了解即可。
stack
stack 是 STL 提供的一种栈。
定义
stack<int> h 定义一个数据类型为 \(int\) 的栈 \(h\)。
需要头文件 #include<stack>。
函数
- 元素访问
h.top()返回 \(h\) 的栈顶元素。
- 元素修改
h.push(int val)在 \(h\) 的栈顶加入元素 \(val\);h.pop()弹出 \(h\) 的栈顶元素。
- 元素个数
h.size()返回 \(h\) 中的元素个数;h.empty()检查 \(h\) 是否为空。
和 vector 基本一致,只是少了很多。
应用
#include <bits/stdc++.h>
using namespace std;
int main()
{
string s;
cin >> s;
int len = s.size();
stack<int> h;
for (int i = 0; i < len; i++)
{
if (s[i] == '.' || s[i] == '@')
continue;
int res = 0;
if (s[i] > '9' || s[i] < '0')
{
int b = h.top();
h.pop();
int a = h.top();
h.pop();
if (s[i] == '+')
res = a + b;
if (s[i] == '-')
res = a - b;
if (s[i] == '*')
res = a * b;
if (s[i] == '/')
res = a / b;
h.push(res);
}
else
{
while (s[i] != '.')
res = res * 10 + s[i] - '0', i++;
h.push(res);
}
}
cout << h.top() << '\n';
return 0;
}
queue
queue 是 STL 提供的一种队列。
定义
queue<int> h 定义一个数据类型为 \(int\) 的队列 \(h\)。
需要头文件 #include<queue>。
函数
- 元素访问
h.front()返回 \(h\) 的队首元素;h.back()返回 \(h\) 的队尾元素。
- 元素修改
h.push(int val)在 \(h\) 的队尾加入元素 \(val\);h.pop()弹出 \(h\) 的队首元素。
- 元素个数
h.size()返回 \(h\) 中的元素个数。h.empty()检查 \(h\) 是否为空。
和 stack 基本一致,只是多了一些。
特殊队列:双端队列 deque
deque<int> h 定义一个数据类型为 \(int\) 的双端队列 \(h\)。
需要头文件 #include<deque>。
- 元素访问
h.front()返回 \(h\) 的队首元素。h.back()返回 \(h\) 的队尾元素。h[x]返回 \(h\) 下标为 \(x\) 的元素。
- 元素修改
h.push_front(int val)在 \(h\) 的队首加入元素 \(val\)。h.push_back(int val)在 \(h\) 的队尾加入元素 \(val\)。h.pop_front()弹出 \(h\) 的队首元素。h.pop_back()弹出 \(h\) 的队尾元素。
- 元素个数
h.size()返回 \(h\) 中的元素个数。h.empty()检查 \(h\) 是否为空。
由于 deque 有着特殊的空间管理,尽量别开太多以避免 MLE(血的教训)。
特殊队列:优先队列 priority_queue
priority_queue 是 STL 提供的一种二叉堆,默认大根堆。
priority_queue<int> h 定义一个数据类型为 \(int\) 的优先队列 \(h\)。
若需要小根堆,可以使用 priority_queue<int, vector<int>, greater<int>> h 或自定义排序规则。
需要头文件 #include<queue>。
greater<> 需要头文件 #include<functional>。
- 元素访问
h.top()返回 \(h\) 中的最大值(最小值)。
- 元素修改
h.push(int val)在 \(h\) 中的加入元素 \(val\),时间复杂度 \(O(\log n)\)。h.pop()弹出 \(h\) 中的最大值(最小值),时间复杂度 \(O(\log n)\)。
- 元素个数
h.size()返回 \(h\) 中的元素个数。h.empty()检查 \(h\) 是否为空。
应用
-
普通队列:BFS / SPFA
struct node { int x, y; }; int dx[8] = {2, 2, 1, 1, -1, -1, -2, -2}, dy[8] = {1, -1, -2, 2, 2, -2, 1, -1}; queue<node> h; h.push({x, y}); vis[x][y] = 1; while (h.size()) { node now = h.front(); h.pop(); for (int i = 0; i < 8; i++) { int xx = now.x + dx[i], yy = now.y + dy[i]; if (xx > 0 && yy > 0 && xx <= n && yy <= m && vis[xx][yy] == 0) { h.push({xx, yy}); ans[xx][yy] = ans[now.x][now.y] + 1; vis[xx][yy] = 1; } } }vector<int> dis(n + 1, INF), vis(n + 1); dis[s] = 0, vis[s] = 1; queue<int> h; h.push(s); while (h.size()) { int now = h.front(); h.pop(); vis[now] = 0; for (int i = fir[now]; i; i = nex[i]) { int p = pos[i]; if (dis[p] > dis[now] + val[i]) { dis[p] = dis[now] + val[i]; if (vis[p]) continue; vis[p] = 1; h.push(p); } } } -
优先队列:堆优
dijkstrastruct node { int pos, val; node() {} node(int p, int v) : pos(p), val(v) {} bool operator<(const node &b) const { return val > b.val; } }; vector<int> dis(n + 1, INF), vis(n + 1); priority_queue<node> h; h.push(node(s, 0)); dis[s] = 0; while (h.size()) { int now = h.top().pos; h.pop(); if (vis[now]) continue; vis[now] = 1; for (auto [p, val] : e[now]) { if (dis[p] > dis[now] + val) { dis[p] = dis[now] + val; h.push(node(p, dis[p])); } } }不会 SPFA 和堆优 dij 的可以查看此博客。
bitset
可以理解为 bool 数组,也可以是一个很大的二进制数。
bitset<N> v
- 代表定义一个长度为 \(N\) 的 \(bool\) 数组 \(v\);
- 代表定义一个 \(N\) 位的二进制数 \(v\)。
值得注意的是,\(bitset\) 只接受常量长度。
需要头文件 #include<bitset>。
应用
-
bitset代替bool数组的埃式筛:#include <bits/stdc++.h> using namespace std; const int N = 1e8 + 7; int n, m; vector<int> p; bitset<N> v; void ai_shai(int n) { v[0] = v[1] = 1; for (int i = 2; i <= n; i++) { if (!v[i]) { p.push_back(i); for (int j = (i << 1); j <= n; j += i) v[j] = 1; } } } int main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> n >> m; ai_shai(n); while (m--) { int x; cin >> x; cout << p[x - 1] << '\n'; } return 0; } -
输出为二进制(包含位运算)
int n, m; cin >> n >> m; bitset<8> a(n), b(m); cout << a << '\n'; cout << b << '\n'; cout << (a | b) << '\n'; cout << (a ^ b) << '\n'; cout << (a & b) << '\n';
set
set 是 STL 提供的集合,能实现平衡树的部分操作,内部通常由红黑树(一种很高效的平衡树)实现。
set 不支持重复元素,若需要用到多个相同元素,可使用 multiset。
定义
set<int>T 定义一个数据类型为 \(int\) 的集合 \(T\)。
需要头文件 #include<set>。
函数
set 的大多数操作的复杂度均为 \(O(\log n)\),不再赘述。
- 元素访问
T.begin()返回一个迭代器,指向 \(T\) 的第一个元素的位置;T.end()返回一个迭代器,指向 \(T\) 最后一个元素的后一个位置;T.rbegin()返回一个逆向迭代器,指向 \(T\) 的最后一个元素的位置;T.rend()返回一个逆向迭代器,指向 \(T\) 的第一个元素的前一个位置;T.count(int val)返回 \(T\) 中元素 \(val\) 的个数;T.find(int val)返回一个迭代器,指向元素 \(val\),不存在则返回 \(T.end()\);T.lower_bound(int val)返回一个迭代器,指向 \(T\) 中第一个不小于 \(val\) 的元素位置,不存在则返回 \(T.end()\);T.upper_bound(int val)返回一个迭代器,指向 \(T\) 中第一个大于 \(val\) 的元素位置,不存在则返回 \(T.end()\)。
- 元素修改
T.clear()清空 T;T.insert(int val)在 \(T\) 中插入一个元素 \(val\),并返回一个 \(pair\),first为迭代器,表示插入位置,second为布尔值,表示是否插入成功;T.erase(int val)删除所有与 \(val\) 相等的元素,并返回删除元素个数;T.erase(iterator pos)删除 \(pos\) 位置的元素,并返回指向下一个迭代器;T.erase(iterator sta, iterator end)删除位于 \([sta,end)\) 之间的元素,并返回指向下一个迭代器。
- 元素个数
T.size()返回 \(T\) 中的元素个数;T.empty()检查 \(T\) 是否为空。
应用
-
部分代替平衡树
其实本质就是一颗红黑树,虽然有些平衡树的操作不能实现。
#include <bits/stdc++.h> using namespace std; int op, w, v, ansb, ansv; struct Flower { int b, val; bool operator<(const Flower &b) const { return val < b.val; } }; set<Flower> T; int main() { while (1) { cin >> op; if (op == -1) break; if (op == 1) { cin >> w >> v; T.insert({w, v}); } if (op == 2 && T.size()) T.erase(--T.end()); if (op == 3 && T.size()) T.erase(T.begin()); } for (auto [b, val] : T) ansb += b, ansv += val; cout << ansb << ' ' << ansv << '\n'; return 0; } -
又叫颜色段均摊,了解即可。
map
map 是 STL 提供的映射,由键对应值而构成键值对,内部通常由红黑树(一种很高效的平衡树)实现。
map 不支持重复键,若需要用到多个相同键,可使用 multimap。
定义
map<string, int>T 定义一个键为 \(string\)、值为 \(int\) 的映射 \(T\)。
需要头文件 #include<map>。
函数
map 的大多数操作复杂度均为 \(O(\log n)\),不再赘述。
- 元素访问
T[string val]返回一个整数,即键 \(val\) 所映射的值,当 \(val\) 对应的映射值不存在时,会插入一个默认值;T.begin()返回一个迭代器,指向 \(T\) 的第一个键值对的位置;T.end()返回一个迭代器,指向 \(T\) 的最后一个键值对的后一个位置;T.rbegin()返回一个逆向迭代器,指向 \(T\) 的最后一个键值对的位置;T.rend()返回一个逆向迭代器,指向 \(T\) 的第一个键值对的前一个位置;T.count(string val)返回 \(T\) 中键为 \(val\) 的个数;T.find(string val)返回一个迭代器,指向键为 \(val\),若不存在返回 \(T.end()\);T.lower_bound(string val)返回一个迭代器,指向 \(T\) 中第一个键不小于 \(val\) 的键值对位置,不存在则返回 \(T.end()\);T.upper_bound(string val)返回一个迭代器,指向 \(T\) 中第一个键大于 \(val\) 的键值对位置,不存在则返回 \(T.end()\)。
- 元素修改
T.clear()清空 \(T\);T.insert(pair<string, int> P)在 \(T\) 中插入一个键值对 \(P\),并返回一个 \(pair\),first为迭代器,表示插入位置,second为布尔值,表示是否插入成功;T.erase(string val)删除所有键为 \(val\) 的键值对,并返回删除元素个数;T.erase(iterator pos)删除 \(pos\) 位置的键值对,并返回指向下一个迭代器;T.erase(iterator sta, iterator end)删除位于 \([sta,end)\) 之间的元素,并返回指向下一个迭代器。
- 元素个数
T.size()返回 \(T\) 中的元素个数;T.empty()检查 \(T\) 是否为空。
特殊 map:哈希 unordered_map
简称为 umap。
umap 是基于哈希表的键值对。在不保证顺序的情况下,umap 提供平均 \(O(1)\) 复杂度的查询、插入和删除。
操作基本同 map。
不过在最坏复杂度下,umap 的单次操作复杂度为 \(O(n)\)。
在竞赛情况下,默认 umap 是被卡的,慎用。
应用
-
(当时练习堆排序和归并排序的受害者就是它)
当值域很大( \(a_i \le 10^9\) )或者不能作为下标(如
string)时,直接开数组进行堆排序/计数排序,编译器不允许的,这时就可以利用map。int n; cin >> n; map<int, int> T; for (int i = 1; i <= n; i++) { int x; cin >> x; T[x]++; } for (auto [val, cnt] : T) for (int i = 0; i < cnt; i++) cout << val << ' '; -
不完全取代离散化
其实作用还是当桶,但时间复杂度不是很优(毕竟
map的操作基本是大常数的 \(O(\log n)\) 复杂度的)。比如这个题。
map仅 \(80\) 分,正解为离散化。#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7; int n, m, len, a[N]; int bel[N], L[400], R[400]; map<int, short> T[400]; void change(int x, int k) { T[bel[x]][a[x]]--; a[x] = k; T[bel[x]][a[x]]++; } int query(int l, int r, int k) { int res = 0; for (int i = l; i <= r; i++) res += (a[i] == k); return res; } int ask(int l, int r, int k) { int lin = bel[l], rin = bel[r]; if (lin == rin) return query(l, r, k); int res = query(l, R[lin], k) + query(L[rin], r, k); for (int i = lin + 1; i < rin; i++) res += T[i][k]; return res; } int main() { cin >> n >> m; len = sqrt(n); for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= len; i++) L[i] = R[i - 1] + 1, R[i] = i * len; R[len] = n; for (int i = 1; i <= len; i++) for (int j = L[i]; j <= R[i]; j++) bel[j] = i, T[i][a[j]]++; for (int i = 1; i <= m; i++) { string op; int x, y; cin >> op >> x >> y; if (op == "C") change(x, y); else { int k; cin >> k; cout << ask(x, y, k) << '\n'; } } return 0; }
string
上述提到的 string 也是一个 STL 容器,专门用来处理字符串。
定义
string s 定义一个字符串 \(s\)。
string s = "abc" 定义一个初始值为 abc 的字符串 \(s\)。
需要头文件 #include<string>。
函数
string 基本可以看作 vector<char> 来使用,但是也有一些不同的操作:
- 对于两个
string a,b,\(a+b\) 实际上是字符串的拼接; - 两个
string a,b的比较大小为比较字典序; s.substr(int pos,int len)截取 \(s\) 中从 \(pos\) 开始的 \(len\) 个字符;s.find(string s1)在 \(s\) 中找到子串 \(s1\) 第一次出现的位置,找不到返回string::npos。
pb_ds
观前提示:并非所有比赛都允许使用
pb_ds 全称 Policy based data structures,俗称平板电视,属于 C++ 拓展库。
包含哈希(hash)、字典树(trie)、可并堆(heap)和平衡树(tree)。
需要头文件:
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp> //hash
#include <ext/pb_ds/trie_policy.hpp> //trie
#include <ext/pb_ds/priority_queue.hpp> //heap
#include <ext/pb_ds/tree_policy.hpp> //tree
和名字空间 __gnu_pbds。
-
hash
hash和map的使用方法类似,只是单次时间复杂度为 \(O(1)\) 而非 \(O(\log n)\)。定义方法有以下两种:
cc_hash_table<int, bool> h; gp_hash_table<int, bool> h;cc 为拉链法,gp 为探测法。据说 gp 较快,cc 比
map还慢。应用
关于 map、umap 和 hash_table 的一些总结。
实现刚刚
mapTLE 的题#include <bits/stdc++.h> #include <ext/pb_ds/assoc_container.hpp> #include <ext/pb_ds/hash_policy.hpp> //用hash using namespace std; using namespace __gnu_pbds; const int N = 1e5 + 7; int n, m, len, a[N]; int bel[N], L[400], R[400]; gp_hash_table<int, short> T[400]; void change(int x, int k) { T[bel[x]][a[x]]--; a[x] = k; T[bel[x]][a[x]]++; } int query(int l, int r, int k) { int res = 0; for (int i = l; i <= r; i++) res += (a[i] == k); return res; } int ask(int l, int r, int k) { int lin = bel[l], rin = bel[r]; if (lin == rin) return query(l, r, k); int res = query(l, R[lin], k) + query(L[rin], r, k); for (int i = lin + 1; i < rin; i++) res += T[i][k]; return res; } int main() { cin >> n >> m; len = sqrt(n); for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= len; i++) L[i] = R[i - 1] + 1, R[i] = i * len; R[len] = n; for (int i = 1; i <= len; i++) for (int j = L[i]; j <= R[i]; j++) bel[j] = i, T[i][a[j]]++; for (int i = 1; i <= m; i++) { string op; int x, y; cin >> op >> x >> y; if (op == "C") change(x, y); else { int k; cin >> k; cout << ask(x, y, k) << '\n'; } } return 0; } -
trie
食用方法:
trie<string, null_type, trie_string_access_traits<>, pat_trie_tag, trie_prefix_search_node_update> t;一般来说,手写
trie足够用了,此处不过多赘述。 -
priority_queue
优先队列,也就是堆。
此处与标准库中的优先队列同名,使用时需要根据名字空间做区分。
pb_ds提供了多种堆的实现,包括:pairing_heap_tag配对堆。binomial_heap_tag二项堆。rc_binomial_heap_tag冗余计数二项堆。binary_heap_tag二叉堆。thin_heap_tag不认识。
其中配对堆的性能最好。
食用方式(以堆优
dij为例):#include <ext/pb_ds/assoc_container.hpp> #include <ext/pb_ds/priority_queue.hpp> //heap struct dijk { int id, val; dijk(int id, int val) : id(id), val(val) {} }; class cmp { public: bool operator()(const dijk &a, const dijk &b) const { return a.val > b.val; } }; __gnu_pbds::priority_queue<dijk, cmp, __gnu_pbds::pairing_heap_tag> h;一般来说,标准库中的优先队列便够用了。
-
tree
tree是平衡树,封装了三棵:rb_tree_tag红黑树。splay_tree_tagsplay 树。ov_tree_tagvector 树。
建议使用红黑树。
详情参见 OI-wiki。
实现 P3369:
#include <bits/stdc++.h> #include <ext/pb_ds/assoc_container.hpp> #include <ext/pb_ds/tree_policy.hpp> #define int long long using namespace std; using namespace __gnu_pbds; tree<int, null_type, less<int>, rb_tree_tag, tree_order_statistics_node_update> T; signed main() { int n; cin >> n; for (int i = 1; i <= n; i++) { int op, k; cin >> op >> k; if (op == 1) T.insert((k << 20) + i); if (op == 2) T.erase(T.lower_bound(k << 20)); if (op == 3) cout << T.order_of_key(k << 20) + 1 << '\n'; if (op == 4) cout << (*T.find_by_order(k - 1) >> 20) << '\n'; if (op == 5) cout << (*(--T.lower_bound(k << 20)) >> 20) << '\n'; if (op == 6) cout << ((*T.upper_bound((k << 20) + n)) >> 20) << '\n'; } return 0; }
rope
前排提示:仅需简单了解,实战基本用不到
同样属于 c++ 拓展库,是一棵可持久化平衡树。
定义
变量定义 rope<int> *h[N]。
需要头文件 #include<ext/rope> 和名字空间 __gnu_cxx。
可持久化 h[i]=new rope<int>(*h[j]);,其中 \(i\) 为当前版本,\(j\) 为某个历史版本。时间复杂度 \(O(1)\)。
函数调用
h[i]->push_back(val)在 \(h[i]\) 的末尾加入 \(val\);h[i]->at(k)访问第 \(k\) 个元素;h[i]->replace(pos,val)将位置为 \(pos\) 的元素换成 \(val\);h[i]->size()返回 \(h[i]\) 的元素个数;h[i]->insert(pos,val)在 \(pos\) 位置插入 \(val\);h[i]->erase(pos,k)从 \(pos\) 位置向后删除 \(k\) 个元素;h[i]->substr(pos,k)从 \(pos\) 位置开始提取 \(k\) 个元素;h[i]->copy(pos,k,q)将从 \(pos\) 位置向后 \(k\) 个元素拷贝到 \(q\) 中。
和 vector 基本一致。
如果维护文艺平衡树(即区间翻转),就维护一正一反两个 rope,区间翻转就把两个 rope 的对应区间 swap 一下就行了。
但是!!
STL 的通病:常数巨大。
并且,rope 空间复杂度极高!!
实现 P3919:
#include <bits/stdc++.h>
#include <ext/rope>
using namespace std;
using namespace __gnu_cxx;
const int N = 1e6 + 7;
rope<int> *a[N];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
a[0] = new rope<int>(0);
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
a[0]->push_back(x);
}
for (int i = 1; i <= m; i++)
{
int var, op, x;
cin >> var >> op >> x;
a[i] = new rope<int>(*a[var]);
if (op == 1)
{
int k;
cin >> k;
a[i]->replace(x, k);
}
else
cout << (a[i]->at(x)) << '\n';
}
return 0;
}
实际得分 72 分,时间空间双重爆炸。
用快读快写也仅只有 80 分。
算法(Algorithm)
基本上都在 #include<algorithm> 库里,以下不再重复。
sort
sort 是 STL 提供的排序算法,也算是最常用的 STL 算法了。
认识了 sort 之后,好像就没怎么手写过排序。什么?你说逆序对?那玩意不是用树状数组吗()
sort,可以对指定区间内的元素进行排序,默认按照从小到大的顺序排列。
其时间复杂度 \(O(n\log n)\),对于大多数竞赛题目,使用 sort 便足够了。
具体用法有:
- 对于普通数组,需要使用数组的首尾指针确定排序区间;
- 对于 STL 容器(常为
vector),需要使用容器迭代器确定排序区间。 - 对于结构体排序,需要重载运算符或自定义排序函数。
值得注意的是,sort 采用左闭右开的区间形式。
常见写法
-
普通数组
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7; int a[N]; int main() { int n; cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; sort(a + 1, a + n + 1); // 左闭右开 for (int i = 1; i <= n; i++) cout << a[i] << ' '; return 0; } -
vector
int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; sort(a.begin(), a.end()); // 左闭右开 for (int i = 0; i < n; i++) cout << a[i] << ' '; -
结构体排序
对于结构体而言,是没有默认的排序方式的,所以需要自定义排序方式。
写法可以为重载运算符和自定义比较函数。
-
重载运算符:
struct node { int x, y; bool operator<(const node &b) const { return (x == b.x) ? (y < b.y) : (x < b.x); } }; int n; cin >> n; vector<node> a(n); for (int i = 0; i < n; i++) cin >> a[i].x >> a[i].y; sort(a.begin(), a.end()); for (int i = 0; i < n; i++) cout << a[i].x << ' ' << a[i].y << '\n' ; -
自定义比较函数:
#include <bits/stdc++.h> using namespace std; struct node { int x, y; }; bool cmp(const node &a, const node &b) { return (a.x == b.x) ? (a.y < b.y) : (a.x < b.x); } int main() { int n; cin >> n; vector<node> a(n); for (int i = 0; i < n; i++) cin >> a[i].x >> a[i].y; sort(a.begin(), a.end(), cmp); for (int i = 0; i < n; i++) cout << a[i].x << ' ' << a[i].y << '\n'; return 0; }也可以写成 lambda 表达式:
struct node { int x, y; }; int n; cin >> n; vector<node> a(n); for (int i = 0; i < n; i++) cin >> a[i].x >> a[i].y; sort(a.begin(), a.end(), [](const node &a, const node &b) { return (a.x == b.x) ? (a.y < b.y) : (a.x < b.x); }); for (int i = 0; i < n; i++) cout << a[i].x << ' ' << a[i].y << '\n';
-
lower_bound 和 upper_bound
STL 提供的一种二分查找算法,分别可以在 \(O(\log n)\) 复杂度里在一个有序数组找到
- 第一个 \(\ge k\) 的位置(
lower_bound); - 第一个 \(>k\) 的位置(
upper_bound)。
对于 STL 容器,其返回值为迭代器(Iterator),且返回值可能为 a.end()。
由于必须是单调不降的有序数组,所以常与 sort 搭配使用。
常见写法
auto it = lower_bound(a.begin(), a.end(), k);
it 代表从 \(a\) 中找到的第一个 \(\ge k\) 的迭代器。
有一点需要注意,set 和 map 内置二分查找函数,复杂度为 \(O(\log n)\),但是对其进行上述二分查找的复杂度则为 \(O(n)\)。
关于迭代器的用法
-
对迭代器取 * 可以得到当前迭代器的值。
判断 k 是否存在:
auto it = lower_bound(a.begin(), a.end(), k); if (it == a.end() || *it != k) cout << "No exist!"; -
迭代器可以加减,直接相减可得到迭代器之间的距离(仅限于随机访问迭代器)
-
迭代器可自增自减,结合 upper_bound 可以查找最后一个 \le k 的位置
auto it = upper_bound(a.begin(), a.end(), k); if (it == a.begin()) cout << "No exist!"; else { it--; cout << *it; }
习题:P1678
unique
unique,唯一的,独一无二的。
作用是去除相邻的重复元素,将不重复的元素放在区间前面,并返回新的尾迭代器。
由此可见,unique 只是“假性删除”,重复元素仍存在数组 \([new_end, old_end)\) 中,真正删除需要靠自己 erase。
时间复杂度 \(O(n)\)。
想要将所有重复元素均去除,则需要保证有序,故而经常和 sort 搭配使用。
应用:离散化。
离散化三件套:sort, unique, lower_bound。
vector<int> bok;
for (int i = 1; i <= n; i++)
cin >> a[i], bok.push_back(a[i]);
sort(bok.begin(), bok.end());
bok.erase(unique(bok.begin(), bok.end()), bok.end());
for (int i = 1; i <= n; i++)
a[i] = lower_bound(bok.begin(), bok.end(), a[i]) - bok.begin();
swap
swap 的使用十分灵活,可以用于交换两个数、容器里的两个元素甚至两个容器,而且时间复杂度通常是 \(O(1)\) 的。
函数原型:
template<typename _Tp> inline void swap(_Tp& __a, _Tp& __b);
应用比较广,但大多数都是起辅助作用,一般不起主导作用。
比如,按秩合并的并查集,需要将较浅的树合并到较深的树上。
当然可以用 if_else,但一般来说,用 swap 更好看。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 7;
int n, m, fa[N], dep[N];
int find(int x) { return (fa[x] ^ x) ? (find(fa[x])) : x; }
void merge(int x, int y)
{
x = find(x), y = find(y);
if (dep[x] > dep[y])
swap(x, y);
fa[x] = y;
if (dep[x] == dep[y])
dep[y]++;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
fa[i] = i;
for (int i = 1; i <= m; i++)
{
int op, x, y;
cin >> op >> x >> y;
if (op == 1)
merge(x, y);
else
cout << (find(x) == find(y) ? "Y\n" : "N\n");
}
return 0;
}
next_permutation
求出当前排列的下一个排列,可以用于解决全排列问题。
若当前排列是全排列的最后一个排列的时候,返回布尔值 false;否则返回布尔值 true。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
int n, a[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
a[i] = i;
do
{
for (int i = 1; i <= n; i++)
printf("%5d", a[i]);
cout << '\n';
} while (next_permutation(a + 1, a + n + 1));
return 0;
}
不常用算法
可能只是对我来说不常用,在此仅罗列,感兴趣者可自行查询如何使用。
- for_each 遍历容器,并进行一些操作。
- min_element 和 max_element 求序列中的最小值和最小值。
- itoa 和 atoi

浙公网安备 33010602011771号