专栏文章

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

算法·理论参与者 84已保存评论 131

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
129 条
当前快照
3 份
快照标识符
@min44r0g
此快照首次捕获于
2025/12/01 20:15
3 个月前
此快照最后确认于
2025/12/02 01:08
3 个月前
查看原文

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

本文对 STL 和部分函数相关的知识进行了总结和梳理,作为一篇在赛前完成的文章,希望它对你的 NOIP 有所帮助。
可以通过此文进行查漏补缺,可能一个甜甜的语法糖或者发现了自己记错的复杂度就能在考场上拯救自己。
本文包括但不限于 STL 容器、STL 函数以及许多少见函数。
全文完全手打,无任何 AI 生成内容。截至本文完结,洛谷上应该没有比本篇文章更全的总结。
欢迎大家在评论区对本文提出意见!

vector

vector 方法

方法作用示例时间复杂度注意点
size获取元素个数v.size()O(1)O(1)
empty容器是否为空v.empty()
push_back末尾插入v.push_back(x)均摊 O(1)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)O(n)O2 优化下为 O(1)O(1)O(n)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)O(1),对于非 trivial 类型(如 string、vector、结构体等)为 O(n)O(n)。下面 deque 相同。

迭代器

一个保存 int 的 vector 迭代器声明为:vector<int>::iterator it=v.begin();,事实上,迭代器的声明可以使用 auto 代替:auto it=v.begin();,后文不再阐述。
可以使用 it++it-- 对 vector 的迭代器进行遍历,同时,可以使用 *it 对迭代器解引用:
CPP
for(auto it=v.begin();it!=v.end();it++){
    cout<< *it <<"\n";
}

queue

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

queue 方法

方法作用示例时间复杂度注意点
size获取元素个数q.size()O(1)O(1)
empty容器是否为空q.empty()
push入队(队尾)q.push(114514)
pop出队(队头)q.pop()
front获取队头q.front()
back获取队尾q.back()
queue 没有迭代器,其默认下是 deque 的一种封装。

stack

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

stack 方法

方法作用示例时间复杂度注意点
size获取元素个数与 vector 类似O(1)O(1)
empty容器是否为空
top返回栈顶元素stk.top()
pop弹出栈顶元素stk.pop()
push压入元素到栈顶stk.push(x)
stack 没有迭代器,其默认下是 deque 的一种封装。
冷知识:stack 和 queue 在 STL 都规定了三种实现方式:deque、list 和 vector,默认下使用 deque。

priority_queue

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

priority_queue 方法

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

重载运算符

当优先队列需要放入结构体时,我们需要重载运算符:
CPP
struct Node{
    int x,y,k;

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

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

deque

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

deque 方法

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

set & multiset

set 和 multiset 又被称为“有序集合”和“有序多重集合”,前者内元素不可以重复,后者可以。其底层实现为红黑树。
与优先队列一样,set 和 multiset 储存的元素必须要定义小于运算。

set & multiset 方法

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

重载运算符

和优先队列相同,两者可以通过重载运算符实现自定义元素排序。
CPP
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(logn)O(\log n),遍历时均摊复杂度 O(1)O(1)

map

map 是一对映射,即 key-value,其底层实现为红黑树。
同上,map 的 key 必须定义了小于运算。

map 方法

方法作用示例时间复杂度注意点
size获取元素个数与 vector 类似O(1)O(1)
empty容器是否为空
begin返回开头迭代器
end返回末尾迭代器返回末尾更后的一位。
insert插入元素s.insert(x)O(logn)O(\log n)
find查找元素s.find(x)不存在返回 s.end()
erase删除元素s.erase(x)
lower_bound查找 x\geq x 的最小的元素s.lower_bound(x)
upper_bound查找 >x> x 的最小的元素s.upper_bound(x)
[]访问 key 对应 values[x]xx 不存在时插入一个空值。
clear清空容器s.clear()O(n)O(n)严格 O(n)O(n)
在使用 s[x] 查询前,应先使用 s.find(x) 判断该值是否存在,防止插入大量空值拖累时空复杂度,对程序优化效果极其显著

重载运算符

CPP
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 值,可以使用指针。
以上面的结构体为例,想要获取结构体中的 kk,可以使用 it->first.k,想要获取 value,可以使用 it->second

unordered_set & unordered_map

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

unordered_set & unordered_map 方法

方法作用示例时间复杂度注意点
size获取元素个数与 vector 类似O(1)O(1)
empty容器是否为空
begin返回开头迭代器
end返回末尾迭代器返回末尾更后的一位。
insert插入元素与 set/map 类似
find查找元素不存在返回 s.end()
erase删除元素
clear清空元素O(n)O(n)严格 O(n)O(n)
由于其底层实现为哈希,可能存在被卡的情况,最坏单次添删查的复杂度会退化到 O(n)O(n)。(实际上 CCF 系列比赛卡 STL 容器的概率为 00。)

自定义哈希

当我们自定义了结构体等内容时,STL 容器无法对其进行自动哈希,此时需要我们手写哈希。
需要对内容判断相等,所以需要重载 == 运算符。
需要“定位桶”,所以需要提供哈希函数。
例如我们拥有一个结构体,想要其中的 xxyy 参与哈希:
CPP
struct Node{
    int x,y,k;
};
重载 == 运算符只需要判定所有参与哈希的值相等即可:
CPP
struct Node{
    int x,y,k;

    bool operator==(const Node &other)const{
        return x==other.x && y==other.y;
    }
};
而哈希函数,只需要将每个值分别乘一个无符号大质数后异或起来即可,自己背几个大质数并不困难。
注意,哈希函数的返回值应该为 size_t 类型:
CPP
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;
以上实现了一个以 xxyy 为哈希内容的 unordered_set。
事实上,将哈希函数放到 Node 内部,整理成以下形式:
CPP
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 其实是一串二进制数,通过按位储存将自己的常数降低至 1w\dfrac{1}{w},其中 ww 为计算机位数,适合特殊情况下的卡常操作。
可以通过 bitset<114514> b 来定义一个长度为 114514114514 的 bitset。

bitset 方法

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

string

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

string 方法

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

STL 函数(包含 GNU)

接下来总结一部分引用 bits/stdc++.h 时可以使用的函数,CCF 系列比赛均可使用。
  • sort(st,ed):对 [st,ed)[st,ed) 进行排序,复杂度 O(nlogn)O(n \log n),可以通过重载小于运算符或者传入比较函数自定义排序方式(以下两种方式效果相同):
    CPP
    bool cmp(int a,int b){return a>b;}
    sort(s+1,s+n+1,cmp);
    
    CPP
    struct Node{
        int a;
        bool operator<(const Node &other)const{
            return a>other.a;
        }
    }s[MN];
    sort(s+1,s+n+1);
    
  • lower_bound(st,ed,val):在有序区间 [st,ed)[st,ed) 中查询第一个 val\geq val 的位置,并返回迭代器,复杂度 O(logn)O(\log n)
  • upper_bound(st,ed,val):同上,不过查询第一个 >val> val 的位置。
  • unique(st,ed):除去 [st,ed)[st,ed) 的相邻相同项,一般配合 sort() 达到去重效果,复杂度 O(n)O(n)
  • reverse(st,ed):翻转 [st,ed)[st,ed) 的内容,复杂度 O(n)O(n)
  • fill(st,ed,val):将 [st,ed)[st,ed) 填充为 valval,复杂度 O(n)O(n)
  • find(st,ed,val):返回 [st,ed)[st,ed) 中第一个等于 valval 的指针,复杂度 O(n)O(n)
  • memcpy(target,original,sz):将以 originaloriginal 起始 szsz 字节的内容复制到 targettarget,复杂度 O(sz)O(sz)
  • __lg(x):返回 log2x\lfloor \log_2x \rfloor,与 log2(x) 作用相同,复杂度 O(1)O(1)
  • __gcd(x,y):返回 gcd(x,y)\gcd(x,y),复杂度 O(logn)O(\log n)
  • max(A)/min(A):返回 AA 中的最大值,A2 |A| \geq 2,复杂度 O(A)O(|A|)。下面的代码就可以返回 55
    CPP
    int mx=max({1,1,4,5,1,4});
    
  • hypot(x,y):返回 x2+y2\sqrt{x^2+y^2},可以简便求出欧几里得距离,复杂度 O(1)O(1),精度问题导致常数较大。
  • abs(x)/fabs(x):返回整型/浮点型的 x|x|,复杂度 O(1)O(1)
  • floor(x)/ceil(x):返回浮点型的 x\lfloor x \rfloorx\lceil x \rceil,复杂度 O(1)O(1)
  • advance(it,num):将迭代器 itit 前进 numnum 位,复杂度在随机访问迭代器(vector 和 deque)时为 O(1)O(1),其他迭代器为 O(num)O(num)
  • prev(it),next(it):返回迭代器 itit 的前驱/后继。
  • stoi(string):将 string 转化为整数。
  • __gnu_cxx::power(x,k):在 bits/extc++.h 库中,是自带的快速幂,类似 std::sort(),可以通过重载乘法运算符或提供乘法函数来自定义,复杂度 O(logn)O(\log n)

时间/随机化函数

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

clock 函数

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

time 函数

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

chrono 类

chrono 类在 std::chrono,平时引用了 using namespace std; 后,可以通过以下方式使用:
  • chrono::steady_clock::now().time_since_epoch().count();
其精度达到 101410^{14},绝对够用,有人卡我吃。

rand 函数

rand() 函数在 Windows 下只能生成 [0,32767][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][L,R] 的整数,函数中的 namenamemt19937/mt19937_64 函数。
CPP
mt19937 wdnmd(time(0));
int rnd=uniform_int_distribution<int>(1,50)(wdnmd);
上面这套丝滑小连招可以从获取一个 [1,50][1,50] 中的随机值。

uniform_real_distribution

这个函数可以生成一个 [L,R)[L,R) 的浮点数,使用方式同上:
CPP
mt19937 wdnmd(time(0));
double rnd=uniform_real_distribution<double>(1,50)(wdnmd);

__builtin 函数

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

位运算相关

函数作用时间复杂度注意点
__builtin_popcount(x)统计 11 的个数O(1)O(1)xx 可为 00
__builtin_ctz(x)统计末尾连续 00 的个数xx 不可为 00
__builtin_clz(x)统计前导连续 00 的个数
__builtin_ffs(x)返回最低位 11 的位置1-index,x=0x=0 时返回 00
以上函数结尾加 ll 均有 long long 版本,如 __builtin_popcountll(x)
__builtin_clz(x) 可以用来求 log2x\log_2x

安全检查

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

inline 关键字

inline 关键字会建议编译器对函数进行内联展开
内联展开可以产生两种作用:
  1. 省去函数调用开销。
    • 不需要压栈、跳转、返回等操作,可以小幅度降低常数。
  2. 可能使编译器进行更多优化。
    • 常量折叠、循环展开。
事实上,编译器会自动判断和完成内联展开。

inline 能用么

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

怎么让编译器更听话

以 GCC 为例:
  • inline __attribute__((always_inline)):要求编译器进行内联。在递归等特殊函数时仍可能被拒绝。
  • __attribute__((noinline)):要求编译器不进行内联。
由于 CCF 使用的 GCC 版本过于老旧,你可能会碰到这种情况,触发条件比较诡异,比较吃运气,祝大家不碰到这种事情。

常见的卡常操作

  • 关闭同步流:关闭同步流可以极大降低 cincout 的常数:
    CPP
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
  • 使用 \n 换行:将 endl 替换为 \n,可以极大降低 cout 的常数。
  • 对于树之类的稀疏图,使用链式前向星而不是 vector 存图。
  • 使用快读快写。
  • 在快读快写的基础上,将 getchar()putchar() 改为 getchar_unlocked()putchar_unlocked(),约有 0.50.5 倍的性能提升,Windows 下无法使用。
  • 在 Windows 下可以使用 _getchar_nolock()_putchar_nolock()
  • 在常量取模时,使用 unsigned long long 代替 long long 会有约 11 倍的性能提升。
  • 选用常数更小/复杂度更低的处理方式,例如将线段树改成树状数组,递归式线段树改为非递归式线段树,欧拉序 LCA 改为 DFS 序 LCA,map 改为 unordered_map 等。
  • pb_ds 库的哈希表对比 unordered_map 常数上有提升,可自行查阅相关文章。

常见的其他操作

  • linux 下,可以使用 time ./file_name 查看程序运行时间。
  • linux 下,可以使用 size ./file_name 查看程序占用内存。
  • linux 下,没有使用文件读写时,可以在命令后加上 <input_file_name>output_file_name 实现自定义文件输入输出,例如下面就是一个读入 game.in,输出到 game.out 的,显示程序 game 运行时间的命令:
    CPP
    time ./game <game.in >game.out
    
  • linux 下,ccf 的标准编译指令为:g++ file_name.cpp -o file_name -std=c++14 -O2 -static
  • 结尾添加以下参数可以避免很多问题:-fsanitize=address,undefined -Wall -Wextra -Wshadow
  • linux 下,可以使用以下命令取消栈空间限制:ulimit -s unlimited。如果你想要指定栈空间大小,可以使用:ulimit -s sz,其中 szsz 单位为 KB。
  • linux 下,可以使用 diff 命令比较文件差异(-Bb 可以忽略行末空格与回车);Windows 下,可以使用 fc 命令比较文件差异。
  • 拒绝在任何情况下使用 vector<bool>,史山实现,极易出错。

特别鸣谢

感谢 K8HellamnmasonxiongW_SUNLaoshan_PLUSCommonAntsKaf_yoUDaydreamWarriorWater_FlowerZMQ_Ink6556LilingfengHalberd_Cease对本文的建议和纠错。

修改记录

2025202511112222 日:改正 bitset 相关复杂度,修改错别字,更改多处不严谨的语法。
2025202511112424 日:改进 vector 相关内容,添加 inline 介绍,修改错误,添加 __builtin 少量函数。
2025202511112424 日:再次修改交审,添加 bitset 相关内容,修改错误,添加函数。

评论

131 条评论,欢迎与作者交流。

正在加载评论...