STL 容器以及各函数及其复杂度
主要是实质、函数、性质。
如果没有特殊提出,那么是常数复杂度。
vector
相当于变长数组。
- 支持随机访问(可以直接查询 \(v_i\)),但是不任意位置 \(O(1)\) 插入。通常为了保证效率,只在末尾加入删除元素。
- size:返回实际长度(通常声明的长度会大于实际长度),也就是包含的元素个数。
- empty:判断是否为空,是返回 \(1\),不是返回 \(0\)。
- clear:直接清空。时间复杂度线性。
- 迭代器:随机访问迭代器(注意),可以直接进行加减操作(作差、求和也可)。
- begin/end:返回第一个和最后一个函数的迭代器,如果想返回值的话,可以用
*v.begin ()
,与数组的 \(a_0\) 效果相同。注意下标是从零开始,所以*a.end ()
是越界访问。 - front/back:返回第一个或最后一个元素,即
a.front () = *a.begin ()
,a.back () = *--a.end ()
。 - push_back/pop_back:在末尾插入或者删除元素。
可以代替邻接表存边,如果想连一条 \(x\) 到 \(y\) 的有向边,那么只需:
v[x].push_back (y) ;
无向边同理。
queue
FIFO,从队尾进入,队头出去。
- push:入队。
- pop:出队。
- front:询问队头元素。
- back:询问队尾元素。
用于 SPFA 和其他的什么东西,只不过快死掉就是了。
priority_queue
默认是一个大根二叉堆(其实不能算队列,但是它叫 queue,那就算到这里面好了)。
- 相比 queue 多了一个 top 函数,可以查询堆顶元素(也就是最大值)。
- 支持重载小于号。
- 也可以实现小根堆,有这么两个方法:
1.插入原数的相反数,这个方法容易废。
2.priority_queue < int ,vector < int > ,greater < int > > q
。
deque
双端队列,相当于一个头朝左和一个头朝右的队列混到了一起,也可以看做一个 queue 和 vector 的结合。
- 相比 queue,它支持随机访问。
- 除了 front 和 back,它具有 begin/end 两个头、尾迭代器。
- 不仅有 push_back 和 pop_back,还有 push_front 和 pop_front(因为是双端)。
- 线性的 clear。
有人用这个玩意写单调队列,但是 STL 让人感觉不靠谱,不如手写。
set
只要你学过高中数学(其实不学也知道),就会知道 set(集合)具有互异性,也就是这个玩意可以自动帮你去重,但是他们同时是有序的默认可以单增排序,所以不具有无序性。
- size/empty/clear 同 vector。
- 迭代器:是双向访问迭代器,不支持随机访问,只能
++
或者--
。 - begin/end 同 vector。
- insert:插入操作,复杂度 \(\mathcal{O}(\log n)\),所以要是插入量大有可能炸掉,但是几率不很大。
- find:在本集合中寻找是否有给定值,要是有返回迭代器,否则返回
s.end ()
,仍然带一个 \(\log\)。 - lower(upper)_bound:形式稍有不同,只需
s.lower(upper)_bound (x)
即可返回迭代器。 - erase:如果给出的是迭代器,就删除迭代器指向位置的元素(\(\mathcal{O}(\log n)\)),如果给出的是值,那么会删除等于这个值的元素(\(\mathcal{O}(k+\log n)\),其中 \(k\) 是被删除的元素个数),在 set 下等效,但是到 multimap 就不一样了。这个玩意好用,但是复杂度让人隐隐约约不太放心。
- count:返回集合中等于给定值的元素的个数,\(\mathcal{O}(k+\log n)\)。
multiset
与 set 大致相同,但是允许重复。
唯一需要注意的是 erase,如果你给的是迭代器当然没有问题,但是如果是数值就需要注意了,因为它可能会删好几个。
map
不是地图,是映射。
其形式为:
map < key_type ,value_type > m ;
通常是把一个比较复杂的(如字符串)键值映射到比较简单的域上(如整数)。
- size/empty/clear/begin/end/迭代器/insert/erase/find:同 set。
- map 支持随机访问,这是最吸引人的地方。
但是由于它自带一个 \(\log\),可能会被爆破,比如我。
unordered_map
这个东西不像 map 一样有序,但是 hash 很喜欢它。
在单个查找的时候比 map 优秀,平均是常数复杂度。
bitset
可以 \(n^2\) 过十万?
顾名思义,是按 bit 存而不是 byte。
复杂度是 \(\mathcal {O}(\frac{n}{w})\),其中 \(w=32\)。
一些函数
- reverse 翻转,用处不大。
- unique 去重,用来离散化,用处也不很大?形式:
int m = unique (a + 1 ,a + n + 1) - (a + 1) ;
返回的是去重后的元素个数。
- random_shuffle 随机打乱,更没用了。
- sort 有用但是常见,所以不说。
- lower(upper)_bound 在这里说过了。
- distance 这个值得说一说。目测出来是距离,需要给出两个参数 from 和 to,对数组来说:
int a[] = {...} ;
auto _ = std :: begin (a) ;
auto __ = std :: end (a) ;
auto count = distance (_ ,__) ;
对于容器来说(拿 vector 举例):
distance (v.begin () ,v.end ()) ;
但是对于 STL 这个函数用到不同的容器上复杂度不同,为肾摸?
不难看出这个函数的原理是计算两个迭代器之间的距离,所以不同种类的迭代器效率不同。
拿 vector 的随机访问迭代器举例,他可以直接进行加减操作,所以复杂度是 \(\mathcal{O}(1)\)。
但是如果是像 set 这样的双向访问迭代器的话,这个迭代器只能疯狂地不停自增或者自减 \(1\),那么复杂度就退化为 \(\mathcal{O}(n)\)。
STL 大法好!
set 直接 \(\mathcal{O}(n\log n)\) 爆过,只要你的数据范围在 \(5\times 10^6\) 以下完全没问题。栗子:
Tomoyuki-Mizuyama 的班级里共有 \(n\)(\(1\le n\le10^6\))个同学,每个同学学号为 \(a_i\),现在他们排成一排准备入场看电影,老师给他们安排了一排座位,恰好为 \(n\) 个,编号从 \(0\) 到 \(n-1\),现在从第 \(1\) 个同学开始,同学们按顺序入座,第 \(i\) 个同学的位置规则如下:
- 若 (\(a_i\bmod n\)) 编号的座位为空,则第 \(i\) 个同学入座。
- 若不为空,则从编号为 (\(a_i\bmod n\))的座位开始往右找到第一个空的座位坐下,如果到 \(n-1\) 号座位都不是空的,则从编号为 \(0\) 开始继续往右找。
现在 TM 想知道每个座位上坐的同学的学号分别是多少。
我知道正解是并查集,\(\mathcal{O}(n\log*n)\) 这个近似线性复杂度更快,但是用 set 这题仍然绰绰有余。
首先注意到每个人肯定是有位置的(废话),那么我们只要考虑如何去找就行了。那么(根据题目)设 \(ans_i\) 表示第 \(i\) 个位置上做的学生编号,那么如何实现呢?
我们考虑到这题座位编号不可能重复,而且座位是升序的,所以自然想到了 set。
首先我们对于学生 \(i\) 查找编号为 \(a_i\bmod n\) 的座位:
- 如果没有人坐,直接落座,并且把这个座位从 set 中删除,这可以避免之后重复计算,用到了 erase 函数。
- 有人坐(那么说明这个位置已经被删除了,我们用 find 函数就查找不到):接下来分为两个步骤,从 \(a_i\bmod n\) 位置向后查找,或者如果找不到就从 \(0\) 开始找。
1.这不就是 upperbound 吗?对于向后查找的部分,我们直接 upperbound 第一个大于 \(a_i\bmod n\) 的位置,如果找到记录并且直接删除,如果找不到(返回了s.end ()
),那么:
2.我们从 \(0\) 到 \(a_i\bmod n-1\) 查找,也就是 upper_bound 第一个大于 \(-1\) 的位置即可,其余操作同上。
代码:
#include <bits/stdc++.h>
#define f(i ,m ,n ,x) for (int i = (m) ;i <= (n) ;i += (x))
using namespace std ;
template < typename T > inline void read ( T &x ) {
x = 0 ;
bool flag (0) ;
register char ch = getchar () ;
while (! isdigit (ch)) {
flag = ch == '-' ;
ch = getchar () ;
}
while (isdigit (ch)) {
x = (x << 1) + (x << 3) + (ch ^ 48) ;
ch = getchar () ;
}
flag ? x = -x : 0 ;
}
const int N = 1e6 + 7 ;
int n ,a[N] ,ans[N] ;
set < int > s ;
int main () {
read (n) ;
f (i ,0 ,n - 1 ,1) {
read (a[i + 1]) ;
s.insert (i) ;
}
f (i ,1 ,n ,1) {
if (s.find (a[i] % n) != s.end ()) {
ans[a[i] % n] = a[i] ;
s.erase (a[i] % n) ;
}
else {
if (s.upper_bound (a[i] % n) != s.end ()) {
set < int > :: iterator it = s.upper_bound (a[i] % n) ;
ans[*it] = a[i] ;
s.erase (it) ;
continue ;
}
else {
set < int > :: iterator it = s.upper_bound (-1) ;
ans[*it] = a[i] ;
s.erase (it) ;
}
}
}
f (i ,0 ,n - 1 ,1) {
cout << ans[i] << " " ;
}
cout << '\n' ;
return 0 ;
}