S 组专题:STL 容器实战 —— set/map、priority_queue、bitset 与 pb_ds

本讲定位:CSP-S 选手的 STL 容器”武器库”。J 组层面的 vector/string/stack/queue 基础用法见《初赛专题(一)》,本讲不重复,只讲 S 组的增量:有序容器(set/map)、堆(priority_queue)、哈希容器(unordered_map)、位容器(bitset),以及 CCF 考场可用的 GNU 扩展 pb_ds。每个容器回答四个问题:什么时候该用它、复杂度是多少、S 组真题里怎么出现、哪里有坑

前置知识:《初赛专题(一):STL 容器与线性结构》、结构体与运算符重载、图的邻接表存储。

适合对象:CSP-S 初赛 + 复赛。初赛的阅读程序近年大量使用 set/map/priority_queue;复赛 T1~T2 正确选容器往往就是”会做”与”写得完”的分界线。

环境提醒:NOI Linux 2.0 的编译选项是 g++ -O2 -std=c++14。C++17 的结构化绑定(auto [k, v] : mp)、string_view正式比赛不能用,本讲所有代码均为 C++14 写法。


1. 容器全景图:按”你需要什么操作”选型

STL 容器不是背出来的,是按需求选出来的。S 组场上的选型逻辑是一张表:

你需要的核心操作 首选容器 单次复杂度 底层结构
按下标随机访问、尾部追加 vector $O(1)$ 动态数组
维护有序集合:插入/删除/前驱/后继/最值 set / multiset $O(\log n)$ 红黑树
键值映射且需要按键有序遍历 map $O(\log n)$ 红黑树
键值映射,只查存在性/计数,不关心顺序 unordered_map 均摊 $O(1)$ 哈希表
反复取最大/最小值 priority_queue $O(\log n)$ 二叉堆
大规模 0/1 状态的位运算 bitset $O(n/64)$ 位压缩数组
查询第 $k$ 小 / 某数的排名 pb_dstree $O(\log n)$ 红黑树+子树大小

两条选型口诀:

  1. 要顺序用树,不要顺序用哈希set/mapunordered 慢一个 $\log$,但换来了有序遍历、lower_bound、前驱后继。只做”出现过没有”的判定时才用 unordered
  2. 只要最值就别上平衡树priority_queue 常数远小于 setset 相对堆的额外能力只有两件事——删除任意元素、查询任意排名位置;用不到就选堆。

2. vector 进阶:离散化模板

基础操作不再重复,S 组必须熟练的是 sort + unique + lower_bound 三件套组成的离散化:值域 $10^9$ 装不下数组时,把出现过的值映射成排名 $1 \sim m$。

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;

int main() {
    int n;
    scanf("%d", &n);
    vector<int> a(n);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);

    vector<int> b = a;                            // 拷贝一份用来做"值 -> 排名"的字典
    sort(b.begin(), b.end());
    b.erase(unique(b.begin(), b.end()), b.end()); // 排序 + 去重,b 就是有序值域

    for (int i = 0; i < n; i++) {
        int rk = lower_bound(b.begin(), b.end(), a[i]) - b.begin() + 1; // a[i] 的排名(1 起)
        printf("%d ", rk);
    }
    return 0;
}

lower_bound(first, last, x)有序区间里二分出第一个 $\ge x$ 的位置,upper_bound 是第一个 $> x$ 的位置,两者相减就是 $x$ 的出现次数。对 vector 用全局版本没问题(随机访问,$O(\log n)$);set/map 必须用成员版本,原因见 3.4 节。

另外两个 S 组常用细节:


3. set 与 multiset:可增删的有序集合