【学习笔记】FHQ Treap
\(\textbf{I. 基本操作}\)
\(\textbf{维护子树大小 / size_up()}\)
和线段树中的 \(\text{push_up}\) 操作类似,目的是维护以一个节点为根的子树的节点个数。
inline void size_up(int id)
{
siz[id]=siz[lc[id]]+siz[rc[id]]+1;
}
//siz[id] 表示以节点 id 为根的子树的节点个数
//lc[id] 是其左孩子编号,rc[id] 是其右孩子编号。
//那么 siz[id] 是其左子树大小加上右子树大小加上一个根。
\(\textbf{分裂操作 / Split()}\)
顾名思义,将一颗 \(\text{FHQ Treap}\) 分裂成两颗。
但是自然不是乱分裂,我们给 \(\text{Split}\) 函数传一个参数 \(div\),表示目标为将这颗 \(\text{FHQ Treap}\) 分裂成两颗树,这两棵树满足一颗的节点权值全部 \(\le div\),另一颗反之,节点权值全部 \(>div\)。
这就相当于人为的把 \([-\infty,div]\) 和 \((div,\infty]\) 隔开。
我们规定,在 \(\text{FHQ Treap}\) 中,满足根节点左子树的权值全部小于根节点,右子树反之。
设分裂后,分裂出的两棵树分别为 \(x,y\)。
那么当我们分裂到某个节点时,若它的权值 \(w\in[-\infty,div]\),那么它的左子树一定全是 \(x\) 的,但是右子树不一定,右子树可能有某一部分是属于 \(y\) 的。
若 \(w\in(div,\infty]\),则同理,它的左子树不一定全部属于 \(y\)。
inline void split(int id,int div,int &x,int &y)
{
if(!id)
{
x=y=0;
return ;
}
if(val[id]<=div)
{
x=id;//先把这整棵树粘到 id 上去。
spilt(rc[id],div,rc[id],y);//然后到右子树,把权值 >div 的点割出去。
}else
{
y=id;
spilt(lc[id],div,x,lc[id]);//同上,到左子树分裂出权值 <=div 的点。
}
size_up(id);//搞完之后记得维护树的大小。
return ;
}
\(\textbf{合并操作 / Merge()}\)
既然有分裂,自然有合并。
话说天下大势,分久必合,合久必分。
平衡树同理(
平衡树满足堆的性质,那么 \(\text{FHQ Treap}\) 也满足。
既然是堆,那么直接合并就行了。
注意在下面的代码中,子树 \(x\) 的最大权值比子树 \(y\) 的最大权值小。
合并方向随机一下,不然容易被卡。
inline int merge(int x,int y)
{
if(!x||!y)
return x|y;//一个为空直接返回不为空的那个。
if(c[x]>c[y])//给每个节点随机一个权值 c,以此决定合并方向。
{
rc[x]=merge(rc[x],y);//把 y 粘到 x 的右子树上
size_up(x);
return x;
}else
{
lc[y]=merge(x,lc[y]);// 同上
size_up(y);
return y;
}
}
\(\textbf{新建节点操作 / Create()}\)
思考一下,一个节点要记录那些信息?
首先,它的左右孩子我们要记录下来。
然后,以它为根的子树大小也要记录。
最后,这个节点的权值 \(w\) 和随机权值 \(c\) 也要记录和生成。
这个生成可以使用内存池的方法。
inline int create(int value)
{
siz[len]=1;//只有一个节点。
v[len]=value;//新建节点的权值
c[len]=rerand();//给它随机一个。
lc[len]=rc[len]=0;
return len++;
}
\(\textbf{插入操作 / Insert()}\)
\(\text{FHQ Treap}\) 在实现插入操作时,直接使用合并和分裂在树上搞。
假设我们要插入值 \(value\)。
这样想,我们先把原来的树拆开,拆成 \([-\infty,value]\) 和 \((value,\infty]\),然后直接把这个插入的节点 \(\text{Merge}\) 上去不就行了。
这样就可以简单的完成 \(\text{Insert}\) 操作了!
inline void insert(int value)
{
int x,y;
split(root,value,x,y);
root=merge(merge(x,create(value)),y);//先把 create(x) 和 x 合并起来,再把 y 粘回去。
}
\(\textbf{删除操作 / Erase()}\)
我们要删除权值为 \(value\) 的点,我们还是考虑像插入一样,先拆,操作后再合并。
先把树分裂成 \([-\infty,value)\)、\(\{value\}\),\((value,\infty]\) 三个区间。
但是有没有一种可能,\(value\) 不止一个但你只删一个。
所以第二颗 \(\{value\}\) 的树删掉一个根就行。
inline void erase(int value)
{
int x,y,z;
split(root,value,x,z);//此时 z 确定为 (value,inf]。
split(x,value-1,x,y);//此时,x 确定为 [-inf,value),y 确定为 {value}。
root=merge(merge(x,/*把根删了*/merge(lc[y],rc[y])),z);//按序合并回来。
}
\(\textbf{查值排名 / Rnk()}\)
显然把这棵树分裂成 \([-\infty,value)\) 和 \([value,\infty]\),然后查左边那区间的长度加一即可。
别忘了合并回来。
inline int rnk(int value)
{
int x,y,ret;
split(root,value-1,x,y);//如上分裂。
ret=siz[x]+1;//记录长度的意义在这。
root=merge(x,y);
return ret;
}
\(\textbf{查询前驱、后继 / Pre()、Nxt()}\)
套路的,分裂再合并。
由于平衡树的性质,查前驱一路往右爬,查后继一路往左爬。
两个代码很像诶,毕竟原理是一致的。
inline int pre(int v)
{
int x,y,p,res=no_sol;
split(root,v-1,x,y);
p=x;
while(p)
{
res=val[p];
p=rc[p];//一路往右爬
}
root=merge(x,y);
return res;
}
inline int pre(int v)
{
int x,y,p,res=no_sol;
split(root,v,x,y);
p=y;
while(p)
{
res=val[p];
p=lc[p];//一路往左爬
}
root=merge(x,y);
return res;
}
\(\textbf{区间第 k 大 / Kth-number()}\)
终于不是分裂再合并了(
平衡树中序遍历是有序序列,所以直接决定当前往左爬还是往右爬即可。
比较简单不多说了。
inline int kth(int value)
{
int p=root;
while(true)
{
if(siz[lc[p]]+1==k)
return val[p];//这些加一都是考虑根。
if(siz[lc[p]]>=k)
p=lc[p];
else
{
k-=siz[lc[p]]+1;
p=rc[p];
}
}
}
\(\textbf{II. 文艺平衡树(正在更)}\)
\(\textbf{懒标记及其下传 / Lazy Tag & push_down()}\)
这玩意不如去看那篇皎月半洒花写的线段树,我觉得介绍懒标记说的挺好的。
在树上翻转显然想到交换左右孩子。但是如果你给它一路翻转下去,容易发现你的复杂度上天,为 \(n\log n\)。使用懒标记,需要时再下传,复杂度变成 \(\log n\),总复杂度维持 \(O(n\log n)\)。
如何下传?难道是 tag[lc[p]]=tag[rc[p]]=true
?
显然不是,如果一个区间翻转了偶数次,就相当于没有翻转。
故代码为:
inline void push_down(int p)
{
if(tag[p])//若懒标记存在
{
swap(lc[p],rc[p]);//交换左右子树
tag[lc[p]]^=1;
tag[rc[p]]^=1;//下传懒标记
//这样写能保证原来是 1 时变为 0,原来为 0 时变为 1。
//也就是取反
tag[p]=0;//清空懒标记。
}
return ;
}
\(\textbf{分裂操作 / Split()}\)
既然是 \(\text{FHQ Treap}\) 那么依然是分裂和合并!
文艺平衡树需要实现神秘的区间翻转,这一点说明我们像上面那样直接按权值分裂是完全不可行的。
换个思路,我们按照树的大小分裂。然后写一段几乎一样的代码即可。
inline void split(int id,int div,int &x,int &y)
{
if(!id)
{
x=y=0;
return ;
}
push_down(id);//你线段树 query/update 之前是不是也要下传?
if(siz[lc[id]]<div)//实际上,是siz[lc[id]]+1<=div,即左子树加上根。
{
x=id;
split(rc[id],div-siz[lc[id]]-1,rc[id],y);//别忘了把根也分裂过去。
}
else
{
y=id;
split(lc[id],div,x,lc[id]);//这几行和普通的 FHQ-Treap 含义差不多
}
size_up(id);//普通的 FHQ-Treap 也要这样。
return ;
}
\(\textbf{合并操作 / Merge()}\)
联想一下线段树,很显然在普通平衡树板子上加个懒标记下传即可。
inline int merge(int x, int y)
{
if (!x||!y)
return x|y;
if (c[x]>c[y])
{
push_down(x);//就这个点喔
rc[x]=merge(rc[x],y);
size_up(x);
return x;
}
else
{
push_down(y);//你不 push_down 你不能确保下面的 lc/rc 正确
lc[y]=merge(x,lc[y]);
size_up(y);
return y;
}
}
\(\textbf{区间翻转操作 / Reverse()}\)
\(\text{FHQ-Treap}\) 伟大的地方就在于你理解了 \(\text{Merge}\) 和 \(\text{Split}\) 之后就会写平衡树了。
inline void reverse(int l,int r)
{
static int x,y,z;
split(rt,l-1,x,y);//x -> [1,l) , y -> [l,n]
split(y,r-l+1,y,z);//y -> [l,r] , z -> (r,n]
tag[y]^=1;// 此时直接把区间 y 打上懒标记!
root=merge(merge(x, y), z);//合并回来,完成!
return ;
}
\(\textbf{输出区间 / Print()}\)
以洛谷的模板为例,你需要输出原序列。
直接从根往下深搜一遍即可,搜到空结点直接 return
。
inline void dfs(int id)
{
if(id==0)//空节点
return ;
push_down(id);
dfs(lc[id]);//先把在该节点之前的输出
printf("%d ",val[id]);//输出该节点,运用了递归的性质。
dfs(rc[id]);//再把后面的输出了
}
\(\textbf{III. 一些有点难度的例题}\)
比较弱智,就是维护几种操作。仔细想想,发现文艺平衡树和普通平衡树都是可以做的。
对于普通平衡树,你只要吧对应的 \(s\) 分裂出来在找到位置合并回去即可。
对于文艺平衡树,以 \(\text{Bottom}\) 为例,$\text{Reverse}(s,top) $ 之后 \(\text{Reserve}(s,top-1)\) 即可。其余同理。
仔细一想,感觉两种写法都差不多,随便写一种过了跑路!