Treap介绍
Treap
,是平衡树的分支之一,故也支持旋转操作,在数据结构中也称树堆,之所以叫树堆,是因为
Treap
=
Tree
(树)+
Heap
(堆)。其基本操作的期望时间复杂度为
O
(
正题
Treap
是一棵二叉排序树,它的左子树和右子树分别是一个
Treap
,和一般的二叉排序树不同的是,
Treap
记录一个额外的数据,就是优先级。
Treap
里面的每一个元素都有一个随机的数值作为优先级,如果就优先级来看,
Treap
将会满足堆的性质(大根堆或小根堆)。由于优先级是随机出来的,因此期望树高为
log
n
。
构建
构建一棵
第一种是较为简单,将数按顺序加入,并给加入的元素附上一个随机优先级,在对元素进行插入操作即可。时间复杂度
O
(
第二种就是直接维护一个栈。考虑到优先级会随着深度的增加而减小,因此可以维护一个栈来完成
如果实在不会第二种用第一种也无妨,毕竟要维护一棵
Treap
就意味着会有询问或修改操作,单次操作的期望复杂度是
O
(
插入
首先先按照二叉搜索树的插入一样将元素插入到一个叶节点上,然后就从下往上维护堆的性质,利用旋转操作,如果儿子节点的优先级比父亲优的话就把儿子节点旋转至他父亲的位置。因为期望树高是
附上代码。
Code:
//tree[x].size是指以x为根的子树大小
//tree[x].v是指以x节点的实际数值
//tree[x].l和tree[x].r分别是指x的左儿子和右儿子
//tree[x].rnd是指x的随机优先级
//leftturn是左旋操作,rightturn是右旋操作
//接下来的都一样,就不加注释了
void insert(int &k,int x)//此Treap为小根堆
{
if(!k)
{
k=++size;
tree[k].size=1;tree[k].v=x;tree[k].rnd=rand();
return;
}
tree[k].size++;
else if(x>tree[k].v)
{
insert(tree[k].r,x);
if(tree[tree[k].r].rnd<tree[k].rnd)leftturn(k);//维护堆性质
}
else
{
insert(tree[k].l,x);
if(tree[tree[k].l].rnd<tree[k].rnd)rightturn(k);
}
}
删除
先找到要删除的那个节点,然后把该节点优先级较大的儿子旋转到自己的位置,直到该节点选转到了叶节点的位置,便可以直接删除了。期望时间复杂度
附上代码。
Code:
void del(int &k,int x)
{
if(!k)return;
if(tree[k].v==x)
{
if(tree[k].l*tree[k].r==0)k=tree[k].l^tree[k].r;//有一个儿子为空
else if(tree[tree[k].l].rnd<tree[tree[k].r].rnd)
rightturn(k),del(k,x);
else leftturn(k),del(k,x);
}
else if(x>tree[k].v)
tree[k].size--,del(tree[k].r,x);
else tree[k].size--,del(tree[k].l,x);
}
查找
查找和一般的二叉排序树一样,但是由于
区别
其次就是 Treap 的结构不是固定的,而像 splay 等平衡树的结构在数据相同的情况下结构的变化都是相同的,而 Treap 对于相同的数据结构不太可能相同,因此如果存在能够卡掉像 splay 等平衡树的情况, Treap 则不会因此而被卡掉,虽然目前为止我也没有见过这种情况。
再然后,
splay
的时间复杂度是 均摊
log
n
的,而
总而言之,
可持久化Treap
如果你认真研究
Treap
,你就会发现普通的
Treap
大多数时候都不能维护别的平衡树能够维护的操作,例如区间翻转操作,因而就出现了可持久化
Treap
。
可持久化
Treap
有两个基本操作——分离(
split
)和合并(
merge
),在这两个操作的基础上
Treap
就能维护更多其他的操作。
分离(split)
分离操作会将一棵
Treap
分离成两棵
Treap
,一般为将一棵大小的
size
的
Treap
分成前
k
个和后
具体实现如下,每次要从以
o
为根的
看看代码理解会更好。
Code:
//change(o)是指重新计算节点o的信息(更新节点o的信息)
//down(o)是指将o的标记下传至儿子节点
//下面merge也是一样
#define fi first
#define se second
typedef pair<int,int> P;
P split(int o,int k)
{
if(!k)return P(0,o);
down(o);
if(size[tree[o].l]>=k){
P ls=split(tree[o].l,k);
tree[o].l=ls.se;
change(o);
return P(ls.fi,o);
}
P ls=split(tree[o].r,k-tree[tree[o].l].size-1);
tree[o].r=ls.fi;
change(o);
return P(o,ls.se);
}
合并(merge)
函数
期望复杂度为树高,即
O
(
还是看看代码好,一开始我都是看代码看会的,很好理解,合并的代码如下
Code:
int merge(int a,int b)
{
if((!a)||(!b))return a^b;
down(a);
down(b);
if(tree[a].rnd>tree[b].rnd){
tree[a].r=merge(tree[a].r,b);
change(a);
return a;
}
tree[b].l=merge(a,tree[b].l);
change(b);
return b;
}
可持久化Treap的其他操作
注释:接下来的 root 都是整个序列对应的 Treap 根节点,也代表着整棵 Treap 。
插入
对于插入操作,例如我要在
root
的第
k
个位置(即在前
还是那句话,看看标程帮助理解。
Code:
#define fi first
#define se second
typedef pair<int,int> P;
int insert(int root,int x,int k)
{
P ls=split(root,k);
tree[++size]=x;
tree[size].rnd=rand()%123456789;
tree[size].size=1;
root=merge(ls.fi,size);
root=merge(root,ls.se);
return root;
}
删除
对于删除操作,例如我要删除
root
的第
k
个位置上的数,那就先
看看代码。
Code:
#define fi first
#define se second
typedef pair<int,int> P;
int del(int root,int k)
{
P ls=split(root,k);
P s=split(ls.fi,k-1);
root=merge(s.fi,ls.se);
return root;
}
区间修改和询问
有了
利用
split
操作,将整棵
Treap
分成三棵
Treap
,分别为
1
~
询问也差不多是这样吧,都是分成三棵子树。
这个就没有必要写什么代码了,自己理解就好。
可持久化操作
仔细想一下,对于每次操作,都是由多个的 split 和 merge 组成,然而对于每一个 split 和 merge 操作,每一次改变的点数(信息发生变化的点的个数)期望为 log n 个,故当然可以持久化。
总结
相对于普通的
可持久化
Treap
大部分时候都可以取代
splay
,相比
splay
又更容易实现和调试,除了不能用于
LCT
,但陈
bg
大神说他找到用可持久化
Treap
维护
LCT
的方法,具体情况我也不是很清楚。
除此之外,可持久化 Treap 还能做到别的平衡树做不到的操作,也就是说有些题只能用可持久化 Treap 来做,这种题一般一定要用到 split 来完成一些奇怪的操作。
如果有什么错误或可以改进的地方,还请各位大佬指出,谢谢。
就这么多了,希望能够帮助到大家。