前置芝士(了解即可啦~):C++、BST 二叉搜索树、堆、二叉堆
Treap 的概念
Treap 树堆,即树(Tree)+堆(Heap),是一棵弱平衡的二叉搜索树(BST),能同时满足二叉搜索树与堆的性质。
BST 满足任意一个节点的权值都大于等于左子树所有点的权值,且小于等于右子树所有点的权值的性质,我们可以用来求数的排名、前驱和后继,比如这是一棵可爱的 BST:
众所周知,当添加点的权值依次递增或递减时,一般 BST 将会退化成丑陋的链,such as:
那么每次添加点的时间复杂度便能被卡成 (O(n)),(n) 大一点就直接 TLE,寄。
为了规避一般 BST 容易退化成链的问题,Treap 光荣产生力!
Treap 在 BST 的基础上,为每个节点赋上了(随机的)优先值,并在建树时时刻维护堆的性质,由于随机性,建出来的树(也是一种二叉堆)深度期望大小为 (log n),因此规避了此类问题。
常规 Treap 的两种写法
常规 Treap 有两种写法,Splay 与 FHQ Treap:
Splay 为有旋 Treap,貌似是依靠左旋右旋节点的伸展操作来维护的 Treap,可以康康具体的图:
详细戳我
FHQ Treap 即无旋 Treap,与 Splay 不同,它只依靠分裂、合并能够进行添加节点、查询排名等等操作,分裂、合并如图:
FHQ Treap 相对来说代码量较小,也更好理解,应该更适合新手学习吧?
正题:FHQ Treap
FHQ Treap,其中 FHQ 指此做法的发明者——范浩强神犇,是依赖于分裂合并操作实现的 Treap,这种操作方式使得它天生支持维护序列、可持久化等特性,可持久化就以后再补吧。
基础节点信息、建点
首先我们需要一个 (rt) 表示树堆的根,(l)、(r) 表示左右儿子的 (id),还要存一个自己的权值 (key),当前子树大小 (siz),以及优先值 (pri),有必要的话还要存一个 (lazy) 标记。此外还需要一个更新节点信息的函数,当然,随机函数也可以自己手写 qwq,新建点就比较简单,代码如下:
#define uLL unsigned long long
......
int ...rt, gs...;
uLL sd=1;
uLL rd() {
return sd=sd*1145141ull*1145141ull;
}
struct node {
int l, r, siz, key, lazy;
uLL pri;
}tr[100005];
int blt(int key) {//新建点
++gs;
tr[gs].key=key;
tr[gs].lazy=0;
tr[gs].l=tr[gs].r=0; tr[gs].siz=1;
tr[gs].pri=rd();
return gs;
}
void upup(int now) {//更新节点信息
tr[now].siz=tr[tr[now].l].siz+tr[tr[now].r].siz+1;
}
基础的分裂、合并
上文有说到,FHQ Treap 即无旋 Treap,要依靠分裂、合并这两种基础操作。
按权值分裂
分裂有两种,第一种便是上图的按权值分裂,即给定 (key),分裂出所有点权值小于等于 (key) 的树堆 (x) 与所有点权值都大于 (key) 的树堆 (y),此分裂可以用于加点或求前驱、后继,带详解注释的代码如下:
void fl_key(int now, int key, int& x, int& y) {
if(!now) {//当前节点为空
x=y=0;
return ;
}
down(now);//有些题目需要用lazy标记下传,比如文艺平衡树中的区间反转操作
if(tr[now].key
盗来的模拟代码过程的 Gif 图:
上文说过,由于随机性,我们建出来的树堆的深度期望为 (log n) 层,而分裂相当于每层都只搜到了一次,所以分裂一次的时间复杂度是 (O(log_2n))
按子树大小分裂
而第二种是按子树大小分裂,即给定 (siz),优先保留左子树,分裂出大小小于等于 (siz) 的树堆 (x),和剩余的树堆 (y)(可能没有),如图:
这种分裂可以用于一些特殊的操作(比如说文艺平衡树中的翻转区间),在普通平衡树中可用于求排名对应的值,带详解注释代码如下:
void fl_siz(int now, int siz, int& x, int& y) {
if(!now) {//空节点
x=y=0;
return ;
}
down(now);//有些题目需要用lazy标记下传,比如文艺平衡树中的区间反转操作
if(tr[tr[now].l].siz+1
和按权值分裂的时间复杂度一样,(O(log_2n))
合并
合并操作只有一种,但是要求合并的两个树堆 (x)、(y),树堆 (x) 的所有点的权值都要严格小于树堆 (y) 的任意点,(但本蒟蒻还有幸遇见过答辩题要用 fhq treap 合并有交集的树堆的),而此处就要用到我们赋予的优先值啦,如图(可能看起来有点难懂):
如图所示,合并时优先考虑优先值,并以优先的点为合并后的父节点,然后再将非优先点与优先点的儿子合并,这是一个递归的过程,要注意的是合并要从祖先开始,且一般合并是直接合并两个无交集的树,若是有交集那么这个合并就只能用添加点的做法(详见下文),此处直接上代码吧:
int merge(int x, int y) {//合并时需要已经满足x的所有点的权值小于y的所有点的权值
down(x);//有需要的话标记下传
down(y);
if(x == 0 || y == 0) return x+y;//x、y有一个为空节点直接返回非空的节点
if(tr[x].pri
合并的话递归层数取决于树堆深度,而深度期望为 (log n) 层,那么一次合并操作时间复杂度便为 (O(log_2n))
添加点
既然我们已经会分裂、合并了,那么如果想要添加一个值为 (X) 的点,我们可以直接将原树堆按 (key = X),以权值大小分裂,然后新建点,将分裂出的树堆 (x) 和新建点合并后再和分裂出的树堆 (y) 合并,时间复杂度为 (O(log_2n)),如图:
此处为什么不直接合并原树堆和新建点呢?那是因为我们写的 (merge) 合并函数要求合并的是无交集的两树堆,且树堆 (x) 的所有点的权值都要严格小于树堆 (y) 的任意点,所以我们必须要先分裂再合并以保证树堆的正确性,代码如下:
void insert(int key) {
int x, y;
fl_key(rt, key, x, y);
rt=merge(merge(x, blt(key)), y);
}
删除点
删除一个值为 (X) 的点的话可以先按 (key = X),以权值大小分裂得到树堆 (x)、(y),再按 (key = X-1),以权值大小分裂树堆 (x),得到树堆 (z)、(a),然后将树堆 (a) 的根节点丢掉(即合并 (a) 的左右儿子,不合并 (a) 的根节点,这就相当于删了一个点),所有树堆依次合并,时间复杂度依然为 (O(log_2n)),如图:
代码如下:
void dlt(int key) {
int x, y, z;
fl_key(rt, key, x, y);
fl_key(x, key-1, z, x);
rt=merge(merge(z, merge(tr[x].l, tr[x].r)), y);
}
查询排名
若是查询 (X) 的排名,直接按 (key = X-1),以权值大小分裂得到树堆 (x)、(y),答案为树堆 (x) 的 (size+1),时间复杂度为 (O(log_2n)),代码如下:(注意最后要合并回来!)
int ask(int key) {
int x, y, ret=0;
fl_key(rt, key-1, x, y);
ret=tr[x].siz+1;
rt=merge(x, y);
return ret;
}
查询排名对应的值
查询排名 (X) 对应的值,有两种做法。
其一是上文提及的按子树大小分裂,按 (siz = X-1),以子树大小分裂得到树堆 (x)、(y),然后再按 (siz = 1),以子树大小分裂树堆 (y),得到树堆 (a)、(z),(a) 的根节点的值就是答案,时间复杂度为 (O(log_2n)),代码如下:(注意最后都要合并回来!)
int asks(int siz) {
int x, y, z;
fl_siz(rt, siz-1, x, y);
fl_siz(y, 1, y, z);
int ret = tr[y].key;
rt = merge(merge(x, y), z);
return ret;
}
其二是直接硬上,从根节点开始 dfs,如果左儿子+自己的 (size) 比 (X) 小,那么 (X) 减去 (size),搜右儿子,否则就搜左儿子,(X = 0) 时就输出当前点的权值,时间复杂度依旧是 (O(log_2n))(毕竟深度期望为 (log n)),代码如下:
int asks(int siz) {
int o = rt;
while(1) {
int qwq = tr[tr[o].l].siz+1;
if(qwq == siz) break;
if(siz
查询前驱、后继
查询 (X) 的前驱,先按 (key = X-1),以权值大小分裂得到树堆 (x)、(y),然后从树堆 (x) 的根节点开始,右儿子非空就一直往右边走(因为树堆 (x) 的所有点已经满足了其点值小于等于 (X-1),只需找到里面最大的点值),最后输出即可。
查询 (X) 的后继,先按 (key = X),以权值大小分裂得到树堆 (x)、(y),然后从树堆 (y) 的根节点开始,左儿子非空就一直往左边走(因为树堆 (y) 的所有点已经满足了其点值大于 (X),只需找到里面最小的点值),最后输出即可。
时间复杂度均为 (O(log_2n)),代码:(注意最后都要合并回来!)
int findl(int key) {
int x, y, ret=0;
fl_key(rt, key-1, x, y);
ret=x;
while(tr[ret].r != 0) ret=tr[ret].r;
ret=tr[ret].key;
rt=merge(x, y);
return ret;
}
int findr(int key) {
int x, y, ret=0;
fl_key(rt, key, x, y);
ret=y;
while(tr[ret].l != 0) ret=tr[ret].l;
ret=tr[ret].key;
rt=merge(x, y);
return ret;
}
普通平衡树——代码整合
总的时间复杂度大致为 (O(ntimes log_2n)),(n) 表示操作数量。
#include
#define uLL unsigned long long
using namespace std;
int n, opt, k, gs, rt;
uLL sd=1;
uLL rd() {
return sd=sd*1145141ull*1145141ull;
}
struct node {
int l, r, siz, key;
uLL pri;
}tr[100005];
int blt(int key) {
++gs;
tr[gs].key=key;
tr[gs].l=tr[gs].r=0; tr[gs].siz=1;
tr[gs].pri=rd();
return gs;
}
void upup(int now) {
tr[now].siz=tr[tr[now].l].siz+tr[tr[now].r].siz+1;
}
void fl_key(int now, int key, int& x, int& y) {
if(!now) {
x=y=0;
return ;
}
if(tr[now].key
文艺平衡树
建议做完普通平衡树后就直接上这道题,也许能让你对平衡树的理解加深哦 qwq
题目简述
初始有长度为 (n) 的 (1)、(2)、(3)……(n-1)、(n) 的原序列,进行 (m) 次翻转 ([l, r]) 区间的数后,输出当前序列。
题解
既然题目有个平衡树,那我们直接 FHQ Treap 硬上吧!Wait Wait Wait,仔细想想,题目要求我们维护一个序列,那我们一定也要用 FHQ Treap 维护这个序列,但是他需要支持区间翻转操作,那么我们就需要将问题转化一下了 qaq。
对于每一次操作 ([l, r]),我们先假设我们能用 FHQ Treap 分裂得到 ([l, r]) 对应的树堆,接下来我们的问题便是区间翻转了。
假设有一个树:
因为 Treap 是一棵二叉树,而原序列建出来的 Treap 能按中序遍历输出得到原序列,我们可以考虑最后用中序遍历输出答案。若是将图中的树中序遍历,输出为 (c-a-d-u-b),而整个区间翻转就相当于把中序遍历给翻转了,即 (b-u-d-a-c),我们再将新中序遍历结果对应的树画出来:
可以发现,翻转中序遍历结果相当于将树上每个点的左右儿子互换……
OMG,思路不就这么出来了,先用 FHQ Treap 分裂得到 ([l, r]) 对应的树堆,然后翻转每个点的左右儿子,最后再合并回去,这样就 OK 啦!因为直接做翻转每个点的左右儿子会 TLE 穿,所以我们可以存一个 (lazy) 标记,多次翻转的话就每次给标记异或上一个 (1),在分裂、合并时下传标记即可,能够保证不会将标记传到假儿子上,时间复杂度为 (O(mtimes log_2n))
那么我们现在就只需要找到一种分裂方法能够把 ([l, r]) 区间对应的树堆分裂出来就行了,因为有交换左右儿子的操作,那么原 Treap 就不满足二叉搜索树的性质了,我们也因此不能按权值大小分裂,否则就算使其满足了性质,也会将标记下传到假儿子上去。
此时,“按子树大小分裂”的分裂方法就能起到很大的作用了!
有一个显然的性质:对于序列的一个区间内的所有数,在该区间对应的树堆中所对应的节点也是两两相连的。这个性质就能够保证我们按子树大小分裂的正确性啦,我们可以先按 (siz = l-1) 分裂得到树堆 (x)、(y),然后我们再按 (siz = r-l+1) 分裂树堆 (y) 得到树堆 (a)、(z),并给树堆 (a) 打上标记,最后再合并回去,而在分裂、合并时我们再进行标记下传即可。
最后我们按中序遍历输出就是答案惹!
总时间复杂度为 (O(n times log_2n)) 左右,可以接受,代码如下:
#include
#define uLL unsigned long long
using namespace std;
int n, m, l, r, gs, rt;
uLL sd=1;
uLL rd() {
return sd=sd*1145141ull*1145141ull;
}
struct node {
int l, r, siz, key, lazy;
uLL pri;
}tr[100005];
int blt(int key) {
++gs;
tr[gs].key=key;
tr[gs].lazy=0;
tr[gs].l=tr[gs].r=0; tr[gs].siz=1;
tr[gs].pri=rd();
return gs;
}
void upup(int now) {
tr[now].siz=tr[tr[now].l].siz+tr[tr[now].r].siz+1;
}
void fl_key(int now, int key, int& x, int& y) {
if(!now) {
x=y=0;
return ;
}
if(tr[now].key
完结撒花~可持久化以后再来补吧
机房租用,北京机房托管,大带宽租用,IDC机房服务器主机租用托管-价格及服务咨询 www.e1idc.net