Fhq Treap总结~
Fhq Treap 总结
作为一个功能强大的平衡树,Fhq Treap功能强大,代码短,常数大。。
Fhq Treap呢,就像它的名字一般,Fhq指发明者叫做范浩强 Treap是 Tree和Heap的合称,代表它既满足二叉搜索树性质与堆性质。(BST与小根堆性质)
Fhq Treap强大的功能都来源于两个灵魂函数:\(Split\) 与 \(Merge\),也就是分裂与合并
下面以平衡树板子为例子:
首先先列出需要维护的几个值:
struct node{
int ls,rs,siz,val,rnd;
#define ls(x) c[x].ls
#define rs(x) c[x].rs
#define siz(x) c[x].siz
#define val(x) c[x].val
#define rnd(x) c[x].rnd
}c[N];
分裂 Split
分裂分为两种:按大小分裂与按值分裂。先分析按值分裂吧:
首先,一棵Treap,它的任意子树都满足Treap的性质(也就是 \(val\)满足二叉搜索树性质,\(rnd\)满足堆性质)
按值分裂的定义:将一棵平衡树按照 k 分为两棵树 \(x,y\), 其中x中的所有结点的 \(val\) 值均\(\leq k\),y中所有结点的val值均\(\geq k\),当然\(x,y\)也都是一棵Treap,这也就可以继续对 \(x,y\)分裂
首先一棵Treap,它的中序遍历序列一定是满足 \(val\) 递增的,如果说整颗平衡树的值域为\([l,r]\),那么按值分裂分裂出的两颗Treap \(x,y\),x 的值域一定为\([l,k]\),y的值域一定为\([k+1,r]\)。
首先我们建立两个结点 x,y。表示当前分裂的两棵平衡树。
因为Treap的性质,所以当每一个结点的 \(rnd\) 与 \(val\) 确定时,整颗Treap的结构也是确定的,那么当前结点\(now\)将在\(x,y\)的哪一个位置也是确定的。
我们可以通过 &x 与 &y 表示当前结点 \(now\) 应该位于 \(x\) 与 \(y\) 的哪一个位置
如果我们当前到达的结点\(now\)的\(val\leq k\) 那么根据BST性质,结点\(now\)与它的左子树都是\(x\)中的结点,此时直接使 \(x=now\),再将下一个结点归于 \(x\) 树的位置修改为 \(now\) 的右儿子
如果当前到达的结点的\(val > k\)那么当前节点 now 与它的右子树都是 \(y\) 中的结点,此时直接将 \(y=now\),再将下一个结点位于 \(y\) 树的位置改为 \(now\) 的左儿子
inline void Split(int now,int k,int &x,int &y){
if(!now){x=y=0;return;}
if(val(now)<=k) x=now,Split(rs(now),k,rs(now),y);
else y=now,Split(ls(now),k,x,ls(now));
Push_up(now);
return;
}
当然在分裂后还要更新当前的结点的子树大小(也就是Push_up)
inline void Push_up(int x){siz(x)=siz(ls(x))+siz(rs(x))+1;return;}
合并 Merge
首先,基础的合并操作有两个要求:
对于两棵需要合并的Treap (x,y)
- x,y必须满足Treap的性质
- 如果 x的值域为\([l_1,r_1]\), y的值域为\([l_2,r_2]\)的话,必须满足\(r_1\leq l_2\)也就是说 x 中的结点val值必须小于 y 中结点的 val 值。
没有为什么,因为只是基础的合并操作,如果不满足会有些复杂
思考这么一个问题:首先 y 中的所有val值都是大于 x 中的所有val值的,那么 当前x中的结点如果与y中的结点相连,只会有两种位置关系:
1.x 为左儿子,y为根
2.x为根,y为右儿子
而rand(key值)会决定这个关系(因为rand会满足小根堆性质)
所以如果 \(rand(x) < rand(y)\) 那么按照小根堆性质,\(x\) 会在y的上面,而根据二叉搜索树性质,\(y\) 会在\(x\) 的右边。所以此时 \(x\) 为根,\(y\) 为右儿子
而 \(rand(y)<=rand(y)\)时,\(y\) 会在 \(x\) 上面,而 \(y\) 又会在 \(x\) 右边,所以此时 \(y\) 为根,\(x\) 为左儿子
(最后记住合并时记得更新当前的size就可以了)
Code:
inline int Merge(int u,int v){
if(!u||!v) return u+v;
if(rnd(u)<rnd(v)){
rs(u)=Merge(rs(u),v);
Push_up(u);return u;
}
else{
ls(v)=Merge(u,ls(v));
Push_up(v);return v;
}
}
剩下的操作:(都是 \(Split\) 与 \(Merge\) 的应用罢了)
inline int Findkth(int k){//查找排名为 k 的值
int now=root;
while(1){
if(k<=siz(ls(now))) now=ls(now);
else if(k==siz(ls(now))+1) return val(now);
else k=k-siz(ls(now))-1,now=rs(now);
}
}
inline int New_node(int val){
val(++cnt)=val,siz(cnt)=1,rnd(cnt)=rand();
return cnt;
}
inline void Insert(int val){
Split(root,val,x,y);
root=Merge(Merge(x,New_node(val)),y);
return;
}
inline void Delete_all(int val){//删除这个值的所有结点
Split(root,val,x,z);
Split(x,val,x,y);
root=Merge(x,z);
return;
}
inline void Delete_one(int val){//删除一个这个值
Split(root,val,x,z);
Split(x,val,x,y);
y=Merge(ls(y),rs(y));
root=Merge(Merge(x,y),z);
return;
}
inline int Getpre(int val){//查找前驱
Split(root,val-1,x,y);
int now=x;
while(rs(now)) now=rs(now);
root=Merge(x,y);
return val(now);
}
inline int Getnext(int val){//查找后继
Split(root,val,x,y);
int now=y;
while(ls(now)) {cout<<val(now)<<endl;now=ls(now);}
root=Merge(x,y);
return val(now);
}
inline int Findval(int val){//查找排名
Split(root,val-1,x,y);
int res=siz(x)+1;
root=Merge(x,y);return res;
}
例题:
利用Fhq Treap维护序列:
下面先利用文艺平衡树作为例子:
这时的Treap就不同于上面的普通平衡树了:
当前数列的下标满足二叉搜索树性质(而不是 val )而 rnd值依旧满足小根堆性质:
需要反转整个区间,需要用到一个与线段树区间修改很相似的一个懒标记(lazy)表示当前区间是否需要修改
那么既然有 lazy ,那肯定有Push_down啊
如果当前区间需要被反转,交换左右子树即可。
Code:
inline void Push_down(int x){
if(lazy(x)){
swap(ls(x),rs(x));
lazy(ls(x))^=1;lazy(rs(x))^=1;
lazy(x)=0;
}
}
但是维护序列的分裂就不是按值分裂了,而是按照大小分裂。
按大小分裂是指将一棵平衡树按照siz分为 x,y 满足前 siz 个元素都属于 x,其他的都属于y
Code:
inline void Split(int now,int siz,int &x,int &y){
if(!now){x=y=0;return;}
Push_down(now);
if(siz(ls(now))<siz){x=now;Split(rs(now),siz-siz(ls(now))-1,rs(now),y);}
else{y=now;Split(ls(now),siz,x,ls(now));}
Push_up(now);return;
}
而因为整个平衡树上的结点都满足了下标满足BST,rnd满足小根堆,所以整个Merge操作几乎没有变化,(除了会有一个Push_down)
Code:
inline int Merge(int u,int v){
if(!u||!v) return u+v;
if(rnd(u)<rnd(v)){
Push_down(u);rs(u)=Merge(rs(u),v);
Push_up(u);return u;
}
else{
Push_down(v);ls(v)=Merge(u,ls(v));
Push_up(v);return v;
}
}
如果要对\([l,r]\)进行操作:
Split(root,l-1,x,y);
Split(y,r-l+1,y,z);
然后再对 y 树进行操作即可,首先先将整颗Treap进行一次按照 l-1的分裂,那么 y 树中储存的就是 \([l,n]\) 中的序列,再按照区间大小进行分裂,此时 y 树中的区间就是\([l,r]\)了。再给整个区间打上一个反转标记就可以了:
Code:
inline void Change(int l,int r){
Split(root,l-1,x,y);
Split(y,r-l+1,y,z);
lazy(y)^=1;
root=Merge(Merge(x,y),z);return;
}
题外话
在一道五合一的神(shenbi)题出来之前插一个知识点
对于Fhq Treap,建立整个Treap时,一般来说都是将每个点都插入进去,如此时\(O(nlogn)\)的。
但是如果用笛卡尔树线性建立Treap,可以做到\(O(n)\)
对于一棵Treap,每一个结点有两个值,\((x_i,y_i)\),其中,\(x_i\)满足BST性质,\(y_i\)满足小根堆性质,如果我们在插入时保证 \(x_i\) 递增,就可以在线性时间下建立一棵Treap
我们需要维护一条极右链,(也就是从根节点一直走右儿子形成的链)
就像上面的 \(Merge\) 一样
因为插入的 \(x_i\) 递增,所以这个点与之前插入的点就只有两种位置关系
-
之前插入的点为左儿子,先插入的点为根
-
之前插入的点为根,现在插入的点为右儿子
假设现在插入的点 now 为\((x_i,y_i)\)
因为\(y_i\)时满足小根堆性质的,那么插入这个点时,只需要在极右链上找到最下面的 y 值< 当前\(y_i\)的点,再将当前点 now 作为那个点的右儿子即可,如果那个点已经有了右儿子 rs,只需要将rs以及rs的子树接在now的左儿子下就可以了
如果没有找到,那么此时的now就是这颗树的根。
整个过程相当于一个\(y_i\)序列,需要找到当前位置 \(i\),左边的第一个\(y_j<y_i\),这就是单调栈模板了啊!(因为\(x_i\)必须满足BST性质。)
Code:
for(register int i=1;i<=n;++i){
top=siz;
while(top&&val[sta[top]]>val[i]){top--;}//弹出现在不满足条件的
if(top){rs[sta[top]]=i;}//找到了一个满足 yj < yi的,直接成为右儿子
if(top<siz){ls[i]=sta[top+1];}//这个点本身还有一个右儿子
sta[++top]=i;//加入单调栈
siz=top;
}