关于 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>。

函数

  1. 元素访问
  • h.begin() 返回一个迭代器,指向 \(h\) 的第一个元素的位置;
  • h.end() 返回一个迭代器,指向 \(h\) 的最后一个元素的后一个位置;
  • h[x] 返回 \(h\) 下标为 \(x\) 的元素;
  • h.front() 返回 \(h\) 中的第一个数;
  • h.back() 返回 \(h\) 中的最后一个数。
  1. 元素个数
  • h.size() 返回 \(h\) 中的元素个数。
  • h.empty() 检查 \(h\) 是否为空。
  1. 元素修改
  • 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\) 中原有的元素。
  1. 元素修改(不常用)
  • 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\) 操作;

应用

  • 邻接表存图

    1. 定义
    const int N = 1e5 + 7;
    struct edge {
        int to, val;
    };
    vector<edge> e[N];
    
    1. 存图(有向图)
    for (int i = 1; i <= m; i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        e[u].push_back({v, w});
    }
    
    1. 对边进行排序
    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;
    }
    
    1. 遍历
    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>。

函数

  1. 元素访问
  • h.top() 返回 \(h\) 的栈顶元素。
  1. 元素修改
  • h.push(int val) 在 \(h\) 的栈顶加入元素 \(val\);
  • h.pop() 弹出 \(h\) 的栈顶元素。
  1. 元素个数
  • 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>。

函数

  1. 元素访问
  • h.front() 返回 \(h\) 的队首元素;
  • h.back() 返回 \(h\) 的队尾元素。
  1. 元素修改
  • h.push(int val) 在 \(h\) 的队尾加入元素 \(val\);
  • h.pop() 弹出 \(h\) 的队首元素。
  1. 元素个数
  • h.size() 返回 \(h\) 中的元素个数。
  • h.empty() 检查 \(h\) 是否为空。

和 stack 基本一致,只是多了一些。

特殊队列:双端队列 deque

deque<int> h 定义一个数据类型为 \(int\) 的双端队列 \(h\)。

需要头文件 #include<deque>。

  1. 元素访问
  • h.front() 返回 \(h\) 的队首元素。
  • h.back() 返回 \(h\) 的队尾元素。
  • h[x] 返回 \(h\) 下标为 \(x\) 的元素。
  1. 元素修改
  • h.push_front(int val) 在 \(h\) 的队首加入元素 \(val\)。
  • h.push_back(int val) 在 \(h\) 的队尾加入元素 \(val\)。
  • h.pop_front() 弹出 \(h\) 的队首元素。
  • h.pop_back() 弹出 \(h\) 的队尾元素。
  1. 元素个数
  • 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>。

  1. 元素访问
  • h.top() 返回 \(h\) 中的最大值(最小值)。
  1. 元素修改
  • h.push(int val) 在 \(h\) 中的加入元素 \(val\),时间复杂度 \(O(\log n)\)。
  • h.pop() 弹出 \(h\) 中的最大值(最小值),时间复杂度 \(O(\log n)\)。
  1. 元素个数
  • h.size() 返回 \(h\) 中的元素个数。
  • h.empty() 检查 \(h\) 是否为空。

应用

  • 普通队列:BFS / SPFA

    BFS

    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;
            }
        }
    }
    

    ​SPFA

    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);
            }
        }
    }
    
  • 优先队列:堆优 dijkstra

    模板

    struct 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)\),不再赘述。

  1. 元素访问
  • 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()\)。
  1. 元素修改
  • 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)\) 之间的元素,并返回指向下一个迭代器。
  1. 元素个数
  • 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)\),不再赘述。

  1. 元素访问
  • 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()\)。
  1. 元素修改
  • 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)\) 之间的元素,并返回指向下一个迭代器。
  1. 元素个数
  • T.size() 返回 \(T\) 中的元素个数;
  • T.empty() 检查 \(T\) 是否为空。

特殊 map:哈希 unordered_map

简称为 umap。

umap 是基于哈希表的键值对。在不保证顺序的情况下,umap 提供平均 \(O(1)\) 复杂度的查询、插入和删除。

操作基本同 map。

不过在最坏复杂度下,umap 的单次操作复杂度为 \(O(n)\)。

在竞赛情况下,默认 umap 是被卡的,慎用。

如何让 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。

  1. 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 的一些总结。

    实现刚刚 map TLE 的题

    #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;
    }
    
  2. trie

    食用方法:

    trie<string, null_type, trie_string_access_traits<>, pat_trie_tag, trie_prefix_search_node_update> t;
    

    一般来说,手写 trie 足够用了,此处不过多赘述。

  3. 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;
    

    一般来说,标准库中的优先队列便够用了。

  4. tree

    tree 是平衡树,封装了三棵:

    • rb_tree_tag 红黑树。
    • splay_tree_tag splay 树。
    • ov_tree_tag vector 树。

    建议使用红黑树。

    详情参见 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 采用左闭右开的区间形式。

常见写法

  1. 普通数组

    #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;
    }
    
  2. 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] << ' '; 
    
  3. 结构体排序

    对于结构体而言,是没有默认的排序方式的,所以需要自定义排序方式。

    写法可以为重载运算符和自定义比较函数。

    • 重载运算符:

      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)\)。

关于迭代器的用法
  1. 对迭代器取 * 可以得到当前迭代器的值。

    判断 k 是否存在:

    auto it = lower_bound(a.begin(), a.end(), k);
    if (it == a.end() || *it != k)
        cout << "No exist!";
    
  2. 迭代器可以加减,直接相减可得到迭代器之间的距离(仅限于随机访问迭代器)

  3. 迭代器可自增自减,结合 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
posted @ 2026-10-30 14:43  Zvelig1205  阅读(32)  评论(0)    收藏  举报