FHQ Treap学习笔记

\(FHQ\) \(Treap\) 学习笔记

\(FHQ\) 属于平衡树的一种,而 \(FHQ\) 能支持维护值,下标,还可以区间修改,可持久化,非常逆天。

首先,在使用 \(FHQ\) 时我们需要维护两个值 \(val\)\(key\),\(val\) 就是输入的值,\(key\) 是随机生成的一个数,用于帮助我们维护高度,使其不至于炸掉(其实有可能生成一条链,不过这个概率几乎和被核弹直接物理创死的概率一样)而这两个值,需要满足两个性质:

  1. 需要满足小根堆性质(也就是 \(i\) 的左右儿子的 \(val\) 值大于等于 \(i\)\(val\) 值)。

  2. 需要满足 \(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;
}

最后参考资料:

1
2

posted @ 2023-11-23 10:48  Populus_euphratica  阅读(9)  评论(0)    收藏  举报