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)

  1. x,y必须满足Treap的性质
  2. 如果 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;
}

例题:

P1503 鬼子进村

P3369 【模板】普通平衡树

P6136 【模板】普通平衡树(数据加强版)


利用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;
}

AC Code

题外话

在一道五合一的神(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\) 递增,所以这个点与之前插入的点就只有两种位置关系

  1. 之前插入的点为左儿子,先插入的点为根

  2. 之前插入的点为根,现在插入的点为右儿子

假设现在插入的点 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;
}
posted @ 2021-02-25 19:36  NuoCarter  阅读(78)  评论(0编辑  收藏  举报