boxmoe_header_banner_img

Welcome Luogu Pro

加载中

文章导读

STL 与奇技淫巧——考场上的好帮手


avatar
gugugu 2026年2月24日 108

本文对 STL 和部分函数相关的知识进行了总结和梳理,作为一篇在赛前完成的文章,希望它对你的 NOIP 有所帮助。

可以通过此文进行查漏补缺,可能一个甜甜的语法糖或者发现了自己记错的复杂度就能在考场上拯救自己。

本文包括但不限于 STL 容器、STL 函数以及许多少见函数。

全文完全手打,无任何 AI 生成内容。截至本文完结,洛谷上应该没有比本篇文章更全的总结。

欢迎大家在评论区对本文提出意见!

vector

vector 方法

方法作用示例时间复杂度注意点
size获取元素个数v.size()$O(1)$
empty容器是否为空v.empty()
push_back末尾插入v.push_back(x)均摊 $O(1)$。
emplace_back末尾插入v.emplace_back(x)效果相同,常数略小。
pop_back删除末尾v.pop_back()
back获取末尾元素v.back()
front获取开头元素v.front()
begin返回开头迭代器v.begin()
end返回末尾迭代器v.end()返回末尾更后的一位。
[]下标访问元素v[x]v.at(pos) 作用相同。
clear清空容器v.clear()$O(n)$O2 优化下为 $O(1)$ 或 $O(n)$。不释放空间。
resize调整容器大小v.resize(x)不释放空间。
shrink_to_fit尝试释放空间v.shrink_to_fit()尝试释放未使用空间。
insert插入元素v.insert(pos,x)将大量元素向后移动。
erase删除元素v.erase(pos)将大量元素向前移动。

注:

  • @CommonAnts 在讨论区中提到 reserve() 函数可以进行缩容,但经查阅 cppreference,此函数只进行扩容而不缩容,只有 shrink_to_fit() 函数会尝试缩容。
  • 在 O2 优化下,vector::clear() 对于 trivial 类型(如 int、long long、double 等)为 $O(1)$,对于非 trivial 类型(如 string、vector、结构体等)为 $O(n)$。下面 deque 相同。

迭代器

一个保存 int 的 vector 迭代器声明为:vector<int>::iterator it=v.begin();,事实上,迭代器的声明可以使用 auto 代替:auto it=v.begin();,后文不再阐述。

可以使用 it++it-- 对 vector 的迭代器进行遍历,同时,可以使用 *it 对迭代器解引用:

for(auto it=v.begin();it!=v.end();it++){
    cout<< *it <<"\n";
}

queue

queue 又叫队列,是一种先进先出的结构。

queue 方法

方法作用示例时间复杂度注意点
size获取元素个数q.size()$O(1)$
empty容器是否为空q.empty()
push入队(队尾)q.push(114514)
pop出队(队头)q.pop()
front获取队头q.front()
back获取队尾q.back()

queue 没有迭代器,其默认下是 deque 的一种封装。

stack

stack 方法

方法作用示例时间复杂度注意点
size获取元素个数与 vector 类似$O(1)$
empty容器是否为空
top返回栈顶元素stk.top()
pop弹出栈顶元素stk.pop()
push压入元素到栈顶stk.push(x)

stack 又叫栈,是一种后进先出的结构。

stack 没有迭代器,其默认下是 deque 的一种封装。

冷知识:stack 和 queue 在 STL 都规定了三种实现方式:deque、list 和 vector,默认下使用 deque。

priority_queue

priority_queue 优先队列是一个,默认下是大根堆。

priority_queue 方法

方法作用示例时间复杂度注意点
size获取元素个数q.size()$O(1)$
empty容器是否为空q.empty()
top询问堆顶元素q.top()
push把元素插入堆q.push(114514)$O(\log n)$
pop删除堆顶元素q.pop()

重载运算符

当优先队列需要放入结构体时,我们需要重载运算符:

struct Node{
    int x,y,k;

    bool operator<(const Node &other)const{
        return k>other.k;
    }
};

priority_queue<Node> q;

注意到,优先队列的大小是相反的,在上文代码中,以 $k$ 为关键字进行排序时,使用的是大于号,但实际上实现了小根堆。

deque

deque 又叫双端队列,它支持两端插入、删除,随机访问,功能和函数都像是 vector 和 queue 的结合版。

deque 方法

方法作用示例时间复杂度注意点
size获取元素个数与 vector 类似$O(1)$
empty容器是否为空
push_back末尾插入
push_front开头插入
pop_back删除末尾
pop_front删除开头
begin返回开头迭代器
end返回末尾迭代器返回末尾更后的一位。
[]下标访问元素v.at(pos) 作用相同。
clear清空容器$O(n)$O2 优化下可视为 $O(1)$。
insert插入元素将大量元素向后移动。
erase删除元素将大量元素向前移动。
back获取末尾元素与 queue 类似$O(1)$
front获取开头元素

set & multiset

set 和 multiset 又被称为“有序集合”和“有序多重集合”,前者内元素不可以重复,后者可以。其底层实现为红黑树。

与优先队列一样,set 和 multiset 储存的元素必须要定义小于运算。

set & multiset 方法

方法作用示例时间复杂度注意点
size获取元素个数与 vector 类似$O(1)$
empty容器是否为空
begin返回开头迭代器
end返回末尾迭代器返回末尾更后的一位。
insert插入元素s.insert(x)$O(\log n)$
lower_bound查找 $\geq x$ 的最小的元素s.lower_bound(x)
upper_bound查找 $> x$ 的最小的元素s.upper_bound(x)
erase删除元素s.erase(x)multiset 会删除所有相同元素。若删除单个可用 s.erase(find(x))。
count查找等于 $x$ 的元素个数s.count(x)$O(k + \log n)$
clear清空元素s.clear()$O(n)$严格 $O(n)$。

重载运算符

和优先队列相同,两者可以通过重载运算符实现自定义元素排序。

struct Node{
    int x,y,k;

    bool operator<(const Node &other)const{
        return k<other.k;
    }
};

set<Node> q;

上面实现了一个从小到大排序的 set。

迭代器

set 和 multiset 的迭代器仅支持 it++it-- 两个操作,不支持随机访问,单次复杂度 $O(\log n)$,遍历时均摊复杂度 $O(1)$。

map

map 是一对映射,即 key-value,其底层实现为红黑树。

同上,map 的 key 必须定义了小于运算。

map 方法

方法作用示例时间复杂度注意点
size获取元素个数与 vector 类似$O(1)$
empty容器是否为空
begin返回开头迭代器
end返回末尾迭代器返回末尾更后的一位。
insert插入元素s.insert(x)$O(\log n)$
find查找元素s.find(x)不存在返回 s.end()。
erase删除元素s.erase(x)
lower_bound查找 $\geq x$ 的最小的元素s.lower_bound(x)
upper_bound查找 $> x$ 的最小的元素s.upper_bound(x)
[]访问 key 对应 values[x]$x$ 不存在时插入一个空值。
clear清空容器s.clear()$O(n)$严格 $O(n)$。

在使用 s[x] 查询前,应先使用 s.find(x) 判断该值是否存在,防止插入大量空值拖累时空复杂度,对程序优化效果极其显著

重载运算符

struct Node{
    int x,y,k;

    bool operator<(const Node &other)const{
        return k<other.k;
    }
};

map<Node,int> q;

上面实现了一个 key 值为结构体,value 值为 int,从小到大排序的 map。

迭代器

map 和 set 相同,只支持 it++it--,想要获取当前的 key-value 值,可以使用指针。

以上面的结构体为例,想要获取结构体中的 $k$,可以使用 it->first.k,想要获取 value,可以使用 it->second

unordered_set & unordered_map

两者在 set 和 map 的基础上变成了无序形式,其底层实现为哈希,事实上,下面会介绍自定义哈希的方式。

unordered_set & unordered_map 方法

方法作用示例时间复杂度注意点
size获取元素个数与 vector 类似$O(1)$
empty容器是否为空
begin返回开头迭代器
end返回末尾迭代器返回末尾更后的一位。
insert插入元素与 set/map 类似
find查找元素不存在返回 s.end()。
erase删除元素
clear清空元素$O(n)$严格 $O(n)$。

由于其底层实现为哈希,可能存在被卡的情况,最坏单次添删查的复杂度会退化到 $O(n)$。(实际上 CCF 系列比赛卡 STL 容器的概率为 $0$。)

自定义哈希

当我们自定义了结构体等内容时,STL 容器无法对其进行自动哈希,此时需要我们手写哈希。

需要对内容判断相等,所以需要重载 == 运算符。

需要“定位桶”,所以需要提供哈希函数。

例如我们拥有一个结构体,想要其中的 $x$ 和 $y$ 参与哈希:

struct Node{
    int x,y,k;
};

重载 == 运算符只需要判定所有参与哈希的值相等即可:

struct Node{
    int x,y,k;

    bool operator==(const Node &other)const{
        return x==other.x && y==other.y;
    }
};

而哈希函数,只需要将每个值分别乘一个无符号大质数后异或起来即可,自己背几个大质数并不困难。

注意,哈希函数的返回值应该为 size_t 类型:

struct Node{
    int x,y,k;

    bool operator==(const Node &other)const{
        return x==other.x && y==other.y;
    }
};
struct Hash{
    size_t operator()(const Node &a)const{
        return ((size_t)a.x*1145141u) ^ ((size_t)a.y * 1315423911u);
    }
};
unordered_set<Node,Hash> s;

以上实现了一个以 $x$ 和 $y$ 为哈希内容的 unordered_set。

事实上,将哈希函数放到 Node 内部,整理成以下形式:

struct Node{
    int x,y,k;

    bool operator==(const Node &other)const{
        return x==other.x && y==other.y;
    }

    size_t operator()(const Node &a)const{
        return ((size_t)a.x*1145141u) ^ ((size_t)a.y * 1315423911u);
    }
};
unordered_set<Node,Node> s;

也是可以通过编译且效果相同的。

在能够自动哈希的情况下,请不要手写哈希,效率较自动更低。

bitset

bitset 其实是一串二进制数,通过按位储存将自己的常数降低至 $\dfrac{1}{w}$,其中 $w$ 为计算机位数,适合特殊情况下的卡常操作。

可以通过 bitset<114514> b 来定义一个长度为 $114514$ 的 bitset。

bitset 方法

方法作用示例时间复杂度注意点
[]访问第 $x$ 位b[x]$O(1)$
set全部置 $1$b.set()$O(\dfrac{n}{w})$有参数 $x$ 时对第 $x$ 位进行操作,复杂度 $O(1)$。
reset全部置 $0$b.reset()
flip全部取反b.flip()
any是否有 $1$b.any()
none是否全 $0$b.none()
count统计 $1$ 的数量b.count()
&按位与c=a&b对整个 bitset 按位操作。
|按位或c=a|b
“^”按位异或c=a^b
~按位取反c=~b
<<左移c=b<<2溢出丢弃,低位补 $0$。
>>右移c=b>>2溢出丢弃,高位补 $0$。
_Find_first找到第一个为 $1$ 的位置b._Find_first()GCC 私货。
_Find_next找到 $pos$ 后下一个为 $1$ 的位置b._Find_next(pos)
to_string转换为字符串b.to_string()$O(n)$

注:洛谷的表格合并会使异或符号渲染错误,故加引号。

string

string 为我们提供了许多有用的函数,下面只列举非常常用的一部分。

string 方法

方法作用示例时间复杂度注意点
size获取元素个数与 vector 类似$O(1)$
empty容器是否为空
[]下标访问
clear清空 string严格$O(1)$。
swap交换两个字符串s1.swap(s2)实际交换字符串指针。
back获取末尾元素与 queue 类似
front获取开头元素^
substr返回定长字串s.substr(pos,len)$O(len)$
+=末尾添加字符串s+=str
erase删除定长字串s.erase(pos,len)$O(n)$
insert插入指定字串s.insert(pos,str)$O(n+k)$
replace替换指定字串s.replace(pos,len,str)
find查找指定字串s.find("abc")$O(nm)$最坏 $O(nm)$。

STL 函数(包含 GNU)

接下来总结一部分引用 bits/stdc++.h 时可以使用的函数,CCF 系列比赛均可使用。

时间/随机化函数

时间/随机化的函数篇幅不小,故单独总结。

clock 函数

clock() 会返回当前程序运行的时间,不同系统得到的返回值单位不同,可以通过 (double)clock() / CLOCKS_PER_SEC 统一为秒。

time 函数

time(0) 会返回从 $1970$ 年 $1$ 月 $1$ 日到现在的时间,单位为秒,所以每秒内的返回值相同,在随机化时使用容易影响复杂度。

chrono 类

chrono 类在 std::chrono,平时引用了 using namespace std; 后,可以通过以下方式使用:

其精度达到 $10^{14}$,绝对够用,有人卡我吃。

rand 函数

rand() 函数在 Windows 下只能生成 $[0,32767]$ 的随机数,随机化时使用容易影响复杂度。

需要使用 srand(seed) 设置随机数种子,可以直接使用 time(0)

mt19937

其实我们机房喜欢叫它茅台一九九三七

mt19937 可以生成 unsigned int 类型的随机数,mt19937_64 可以生成 unsigned long long 类型的随机数。

以前者举例,可以通过 mt19937 name(seed) 进行定义,使用时直接调用 name()

rand() 相同,随机数种子可以直接使用 time(0)

uniform_int_distribution

直接使用随机数对数据范围取模会使得答案分布不随机。

此时可以使用 uniform_int_distribution<int/long long>(L,R)(name),它会返回一个 $[L,R]$ 的整数,函数中的 $name$ 是 mt19937/mt19937_64 函数。

mt19937 wdnmd(time(0));
int rnd=uniform_int_distribution<int>(1,50)(wdnmd);

上面这套丝滑小连招可以从获取一个 $[1,50]$ 中的随机值。

uniform_real_distribution

这个函数可以生成一个 $[L,R)$ 的浮点数,使用方式同上:

mt19937 wdnmd(time(0));
double rnd=uniform_real_distribution<double>(1,50)(wdnmd);

__builtin 函数

__builtin 是 GCC 独有的一系列操作,CCF 系列比赛可用。

位运算相关

函数作用时间复杂度注意点
__builtin_popcount(x)统计 $1$ 的个数$O(1)$$x$ 可为 $0$。
__builtin_ctz(x)统计末尾连续 $0$ 的个数$x$ 不可为 $0$。
__builtin_clz(x)统计前导连续 $0$ 的个数
__builtin_ffs(x)返回最低位 $1$ 的位置1-index,$x=0$ 时返回 $0$。

以上函数结尾加 ll 均有 long long 版本,如 __builtin_popcountll(x)

__builtin_clz(x) 可以用来求 $\log_2x$。

安全检查

函数作用时间复杂度注意点
__builtin_add_overflow(a,b,&res)检测加法溢出$O(1)$$res$ 存放结果。
__builtin_sub_overflow(a,b,&res)检测减法溢出
__builtin_mul_overflow(a,b,&res)检测乘法溢出

inline 关键字

inline 关键字会建议编译器对函数进行内联展开

内联展开可以产生两种作用:

  1. 省去函数调用开销。
    • 不需要压栈、跳转、返回等操作,可以小幅度降低常数。
  2. 可能使编译器进行更多优化。
    • 常量折叠、循环展开。

事实上,编译器会自动判断和完成内联展开。

inline 能用么

事实上,inline 的作用仅仅是“建议”,编译器可以拒绝你的建议,尤其在使用 O2 以上优化时基本没有效果。

怎么让编译器更听话

以 GCC 为例:

由于 CCF 使用的 GCC 版本过于老旧,你可能会碰到这种情况,触发条件比较诡异,比较吃运气,祝大家不碰到这种事情。

常见的卡常操作

常见的其他操作

特别鸣谢

感谢 @K8He,@llamn,@masonxiong,@W_SUN,@Laoshan_PLUS,@CommonAnts,@Kaf_yoU,@DaydreamWarrior,@Water_Flower,@ZMQ_Ink6556,@Lilingfeng,@Halberd_Cease 对本文的建议和纠错。

修改记录

$2025$ 年 $11$ 月 $22$ 日:改正 bitset 相关复杂度,修改错别字,更改多处不严谨的语法。

$2025$ 年 $11$ 月 $24$ 日:改进 vector 相关内容,添加 inline 介绍,修改错误,添加 __builtin 少量函数。

$2025$ 年 $11$ 月 $24$ 日:再次修改交审,添加 bitset 相关内容,修改错误,添加函数。



评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码