__gnu_cxx::rope的真面目——浅谈可持久化并查集测试
最近又遇到了可以用可持久化并查集解决的问题,尝试用__gnu_cxx::rope水过,结果被卡成暴力分。鉴于已经有前车之鉴——一次模拟赛也是这样,但是BZOJ上的可持久化并查集却能水过,于是我打算做一些实验。
可持久化数组
由于可持久化并查集的基础是可持久化数组,因此我设计了一道测试用的模板题。
为了简单起见,给定数组$a[1\dots n]$,进行$m$次修改操作,然后$q$个询问,求某次修改后某个位置上的数。由于仅用于测试速度,所以数据随机。$0\le a[i]\le 10^9$。
1 | 5 |
1 | 3 |
id | $n,m,q$ |
---|---|
1 | $10^4$ |
2 | $3\times 10^4$ |
3 | $10^5$ |
4 | $3\times 10^5$ |
5 | $10^6$ |
1 |
|
测试在Manjaro下进行,不计输入输出时间,估计折算净内存使用(有较大的误差,仅作参考)。即用CCR-Plus的monitor来获取内存使用(iorun不支持),因此设置限制为“10 s, 2 GB”。在使用前,需要把monitor代码中的“进程被阻塞”判定删掉。
__gnu_cxx::rope
定义在<ext/rope>
中。可以看出,这和pb_ds
一样是一个GNU库,因此只有libstdc++中有。据说也是由SGI开发的,SGI更为著名的是STL。这个模板类原本是为了快速字符串编辑,像是不少古老的文本编辑器题目可以用rope水过。但也可以把存储类型改成int,再加上特殊的存储方式,使复制的复杂度低于线性,于是就成了“可持久化数组”。
使用方法可以查阅man __gnu_cxx::rope
,这可能是用NOI Linux的优势,可以随便查函数。为了实现快速复制,一般用指针管理更快,调用复制构造就可以快速复制了。rope的operator []
和at()
都不能作为左值,修改一个数要用replace()
方法。
1 |
|
id | O0 time | O2 time | mem |
---|---|---|---|
1 | 81 ms | 43 ms | 30.6 MB |
2 | 302 ms | 157 ms | 107.37 MB |
3 | 1184 ms | 639 ms | 425.05 MB |
4 | 4313 ms | 2227 ms | 1425.61 MB |
5 | N/A | N/A | N/A |
librope
宣传的比SGI rope更高效的实现,参见https://gist.github.com/josephg/3474848#file-results。不过仔细观察就可以发现,librope是用跳表实现的,而不是树,而且测试的项目是大量插入和少量删除操作。
不幸的是,librope是用C写的,不支持模板,只支持字符。更讨厌的是只能用C风格字符串初始化和修改,我不得不把int转为4个字符,而且还得保证中间没有0,一种简单的方法是用255进制。
1 |
|
注意要先用gcc编译librope,再用g++编译这份代码。测试结果悲惨的证明,根本不支持快速复制,看来上当了!
id | O0 time | O2 time | mem |
---|---|---|---|
1 | 652 ms | 574 ms | 680.71 MB |
2 | N/A | N/A | N/A |
主席树
最为简单的可持久化数据结构,用作可持久化数组时,非叶节点不用保存值。其实可以把叶节点的值放在左儿子(或右儿子)来节省不少空间。空间复杂度和倍增一样达到$\mathcal O(n\log n)$,每次操作都是$\mathcal O(\log n)$。
1 |
|
id | O0 time | O2 time |
---|---|---|
1 | 5 ms | 7 ms |
2 | 18 ms | 18 ms |
3 | 92 ms | 56 ms |
4 | 334 ms | 215 ms |
5 | 1308 ms | 871 ms |
由于开的是静态数组,内存估计恒定为195 MB。
分块
显然可以做,但是好像复杂度不平衡。
留坑待填
Random Access List
参见https://www.cs.oberlin.edu/~jwalker/ra-list/,一种没听说过的数据结构,还可以做我以前出的数列。
留坑待填
统计
可持久化并查集
按秩合并
一种常见的伎俩,即合并时把少的合并到多的,常称为启发式合并。可以保证并查集的树高为$\mathcal O(\log n)$级别。若还是$m$次操作$q$次询问,复杂度$\mathcal O((m+q)\log^2 n)$。
路径压缩?
据说不能路径压缩,但是其实好像也没有坏处,但会慢不少。在按秩合并的基础上,复杂度$\mathcal O(\alpha m\log n+q\log^2 n)$,因为询问的高度最坏可以达到$\mathcal O(\log n)$。
例题
不要认为这个没什么用,其实有时候可以暴力过一些题。
模板
如果有空,可以用上面的方法去试一下:洛谷P3919、洛谷P3402,当然还有著名的BZOJ 3673、3674。
NOIp2013 货车运输
大意是给定一个无向图,多次询问两点之间的路径,最大化路径上最小边。标准做法是建最大生成树,然后树上倍增。最近才做了这题,然后就开始研究可持久化并查集。
但是我一开始没有想到只有最大生成树上的边有效,于是开始折腾。首先注意到可以二分答案,在并查集上连上大于等于答案的边,就可以判断连通性。复杂度$\mathcal O(qm\log z)$。然后发现不用二分,离线处理,直接从大到小加边,每次判断所有询问。复杂度$\mathcal O(qm)$,这样就是暴力的60分。
还能怎么改进呢?如果用可持久化并查集,就能保存每次加边后的并查集,询问只要二分判断连通即可。然后我就用了SGI rope,结果还是只有60分,剩下的MLE。
我不肯就此罢休,于是改成了主席树。不幸的是还是60分,剩下的RE,恰好是满数据。主席树的节点应该够了呀。搞来大数据才发现其实是写错了,但只有询问足够大才能遇到这种错误。还好这种错误我在写的时候就注意到了,由于我用了路径压缩,如果询问直接调用getf()
就可能会修改前面的版本,而造成后面的版本不兼容。因此要么先复制一份,要么写一个“只读”版本的getf()
。加上按秩合并优化常数,最后终于过了。感觉完全是在作死,比赛不太可能写出来。
复杂度$\mathcal O(\alpha m\log n+q\log^3 n)$,好像很慢的样子,其实后面的3次log不会满的,大约只有2次。但在当年可能还是会被卡常,毕竟标算只有1次。另外后来发现还是不要路径压缩更好,看来传言还是有道理的。
1 |
|
SillyHook’s history
随便取了个名字,我们这届的2017-07-17模拟赛T3(去年暑假集训),也有公开的https://wenku.baidu.com/view/16dc09b8250c844769eae009581b6bd97e19bc74.html。
当年直接用了SGI rope,结果卡成30分,本来可以写写其他部分分的。