FHQ Treap学习笔记
\(FHQ\) \(Treap\) 学习笔记
\(FHQ\) 属于平衡树的一种,而 \(FHQ\) 能支持维护值,下标,还可以区间修改,可持久化,非常逆天。
首先,在使用 \(FHQ\) 时我们需要维护两个值 \(val\) 和 \(key\),\(val\) 就是输入的值,\(key\) 是随机生成的一个数,用于帮助我们维护高度,使其不至于炸掉(其实有可能生成一条链,不过这个概率几乎和被核弹直接物理创死的概率一样)而这两个值,需要满足两个性质:
-
需要满足小根堆性质(也就是 \(i\) 的左右儿子的 \(val\) 值大于等于 \(i\) 的 \(val\) 值)。
-
需要满足 \(BST\) 的性质(也就是 \(i\) 的左儿子的 \(key\) 值小于等于 \(i\) 的\(val\) 值,\(i\) 的右儿子的 \(key\) 值大于等于 \(i\) 的\(val\) 值)。
接下来我们就开始进行 \(FHQ\) 的操作了:
一、\(split\)

所谓的 \(split\) 操作就是分离操作,分离既可以按照权值分,也可以按照下标分,而后者就是文艺平衡树的主要内容,这里暂时不说(暂时鸽一下,后面打算写在解题报告里面)。具体怎么操作看代码吧
//now是现在的位置,k是用来分裂的依据
//当前节点权值如果小于k就放在x中,大于k就放在y中。
void split(int now,int k,int &x,int &y){
if(!now) x=y=0; //如果now不存在,还分个**
else{
if(val[now]<=k){//当前节点权值小于k
x=now; //先把当前节点归入x
split(A[now][1],k,A[now][1],y);
}//向当前节点的右儿子遍历,因为右儿子还有可能大于k
else{//当前节点权值大于k
y=now;//先把当前节点归入y
split(A[now][0],k,x,A[now][0]);
}//向当前节点的左儿子遍历,因为左儿子还有可能小于k
update(now);//最后别忘了合并
}
}
二、\(merge\)

(\(gif\) 有点小炸,那个儿子权值应该是40,还有最后一个9的儿子应该merge在右儿子。毕竟不是我自己做的)
简单来说就是合并操作,还是直接看代码
//目的是将x,y合并成一颗树,要求不能有重合的
int merge(int x,int y){
if(!x || !y) return x+y;//如果其中一个为空,就可以返回另一个,也就是根,这里运用了一点小寄巧
if(key[x]<key[y]){//合并的话我们需要确定一个为根,为了满足小根堆性质,用key值更小的作为根
A[x][1]=merge(A[x][1],y);//将y接到x的右子上,因为我们分裂的时候满足的是y里面更大,所以接到右子上
update(x);//不要忘记要合并一下值
return x;//返回根
}
else{
A[y][0]=merge(x,A[y][0]);//将x接到y的左子上,因为我们分裂的时候满足的是x里面更小,所以接到左子上
update(y);//不要忘记要合并一下值
return y;//返回根
}
}
\(FHQ\) 的核心操作其实就只是 \(merge\) 和 \(split\),而接下来的操作就是依据这两个来实现的。
三、插入
这个就比较简单,依旧是上代码
split(root,a,x,y);//先按照新插入的值a分成两棵树,小于等于a的放在x里面,大于a的放在y里面
root=merge(merge(x,add(a)),y);//先新建一个节点,把这个节点当做是另外一棵独立的子树,与上面分裂的两棵子树合并
四、删除
废话不多说,上代码
split(root,a,x,z);//先按照要删除的值a分成两棵树,小于等于a的放在x里面,大于a的放在z里面
split(x,a-1,x,y);//在把x重新分一下,小于a的放在x里面,刚好等于a的就在y里面
y=merge(A[y][0],A[y][1]);//因为只用删除一个,就把根节点扔掉,合并一下其左儿子和右儿子
root=merge(merge(x,y),z);//把x,y,z合并起来
五、查询某个值得排名
不想多说,还是看代码
split(root,a-1,x,y);//先按照a-1分成两棵树,这样小于a的就在x里面,所以x+1就是a
printf("%d\n",siz[x]+1);//所以a的值的排名就是x的排名加一
root=merge(x,y);//最后就把之前拆开的合并一下就行了
六、查询排名的值
从这里开始需要引入一个新的函数,具体如下
int pm(int now,int k){
while(1){
if(k<=siz[A[now][0]]) now=A[now][0];//如果now的左子的排名大于要查询的值k,now更新为其的左子
else if(k==siz[A[now][0]]+1) return now;//如果now的左子的排名+1等于k,那就证明我们找到了这个点,返回now的值
else k-=siz[A[now][0]]+1,now=A[now][1];//如果now的左子值更小,就要向右子跳,记得k值要剪一下左边的排名,因为是从左子跳过来的
}
}
而查询排名的值就直接用刚才找到的编号对应的值。
七、求x的前驱(前驱定义为小于a,且最大的数)
不想废话了
split(root,a-1,x,y);//先按照a-1分成两棵树,这样小于a的就在x里面,因为x是里面最大的,所以x代表的值就是我们要找的答案
printf("%d\n",val[pm(x,siz[x])]);//用这个函数找一下原本的点的编号,因为之前建树的时候是重新排的编号
root=merge(x,y);//不要忘记最后的合并
八、求x的后继(后继定义为大于x,且最小的数)
最后一个了
split(root,a,x,y);// 按照a分成两棵树,这样大于a的就在y里面,因为y是里面最小的,所以y所代表的值就是我们要找的答案
printf("%d\n",val[pm(y,1)]);//用这个函数找一下原本的点的编号,因为之前建树的时候是重新排的编号
root=merge(x,y);//不要忘记最后的合并
最后的 \(AC\) 代码(比较答辩)
#include<bits/stdc++.h>
using namespace std;
int n;
int t,a,root=0,x,y,z;
int tot,siz[500010];
int val[500010],key[500010];
int A[500010][3];
int add(int x){
val[++tot]=x;
siz[tot]=1;
key[tot]=rand();
return tot;
}
int update(int x){
siz[x]=1+siz[A[x][0]]+siz[A[x][1]];
}
//now是现在的位置,k是用来分裂的依据
//当前节点权值如果小于k就放在x中,大于k就放在y中。
void split(int now,int k,int &x,int &y){
if(!now) x=y=0; //如果now不存在,还分个**
else{
if(val[now]<=k){//当前节点权值小于k
x=now; //先把当前节点归入x
split(A[now][1],k,A[now][1],y);
}//向当前节点的右儿子遍历,因为右儿子还有可能大于k
else{//当前节点权值大于k
y=now;//先把当前节点归入y
split(A[now][0],k,x,A[now][0]);
}//向当前节点的左儿子遍历,因为左儿子还有可能小于k
update(now);//最后别忘了合并
}
}
//目的是将x,y合并成一颗树,要求不能有重合的
int merge(int x,int y){
if(!x || !y) return x+y;//如果其中一个为空,就可以返回另一个,也就是根,这里运用了一点小寄巧
if(key[x]<key[y]){//合并的话我们需要确定一个为根,为了满足小根堆性质,用key值更小的作为根
A[x][1]=merge(A[x][1],y);//将y接到x的右子上,因为我们分裂的时候满足的是y里面更大,所以接到右子上
update(x);//不要忘记要合并一下值
return x;//返回根
}
else{
A[y][0]=merge(x,A[y][0]);//将x接到y的左子上,因为我们分裂的时候满足的是x里面更小,所以接到左子上
update(y);//不要忘记要合并一下值
return y;//返回根
}
}
//这个函数比较类似于递归
int pm(int now,int k){
while(1){
if(k<=siz[A[now][0]]) now=A[now][0];//如果now的左子的排名大于要查询的值k,now更新为其的左子
else if(k==siz[A[now][0]]+1) return now;//如果now的左子的排名+1等于k,那就证明我们找到了这个点,返回now的值
else k-=siz[A[now][0]]+1,now=A[now][1];//如果now的左子值更小,就要向右子跳,记得k值要剪一下左边的排名,因为是从左子跳过来的
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>t>>a;
if(t==1){
split(root,a,x,y);//先按照新插入的值a分成两棵树,小于等于a的放在x里面,大于a的放在y里面
root=merge(merge(x,add(a)),y);//先新建一个节点,把这个节点当做是另外一棵独立的子树,与上面分裂的两棵子树合并
}
if(t==2){
split(root,a,x,z);//先按照要删除的值a分成两棵树,小于等于a的放在x里面,大于a的放在z里面
split(x,a-1,x,y);//在把x重新分一下,小于a的放在x里面,刚好等于a的就在y里面
y=merge(A[y][0],A[y][1]);//因为只用删除一个,就把根节点扔掉,合并一下其左儿子和右儿子
root=merge(merge(x,y),z);//把x,y,z合并起来
}
if(t==3){
split(root,a-1,x,y);//先按照a-1分成两棵树,这样小于a的就在x里面,所以x+1就是a
printf("%d\n",siz[x]+1);//所以a的值的排名就是x的排名加一
root=merge(x,y);//最后就把之前拆开的合并一下就行了。
}
if(t==4){
printf("%d\n",val[pm(root,a)]);
}
if(t==5){
split(root,a-1,x,y);//先按照a-1分成两棵树,这样小于a的就在x里面,因为x是里面最大的,所以x代表的值就是我们要找的答案
printf("%d\n",val[pm(x,siz[x])]);//用这个函数找一下原本的点的编号,因为之前建树的时候是重新排的编号
root=merge(x,y);//不要忘记最后的合并
}
if(t==6){
split(root,a,x,y);// 按照a分成两棵树,这样大于a的就在y里面,因为y是里面最小的,所以y所代表的值就是我们要找的答案
printf("%d\n",val[pm(y,1)]);//用这个函数找一下原本的点的编号,因为之前建树的时候是重新排的编号
root=merge(x,y);//不要忘记最后的合并
}
}
return 0;
}

浙公网安备 33010602011771号