某科学的无旋Treap(FHQ-Treap)

某科学的无旋Treap(FHQ-Treap)

前置知识

总述

先ORZ一发拯救万千OIER于平衡树苦海之中的范浩强神仙(○| ̄|_

FHQ-Treap,又名无旋Treap。

FHQ-Treap并非像有旋Treap一样依靠旋转而是以分裂和合并两个操作来维护树的平衡,这种操作方式使得它天生支持维护序列、可持久化等特性。

注意FHQ-Treap中不可以有对相同点权的点的去重操作。

分裂(split)

分裂操作和两个参数有关,Treap的根节点 i 以及关键值 Key

分裂操作实际上就是按照关键值将Treap裁成排名或者点权小于等于 Key 和大于 Key 的两棵Treap。

如果当前点 i 的排名或点权小于等于 Key ,那么在分裂时 ii左子树上所有点的排名或点权显然都小于等于 Key ,也就是都会被分入第一部分

如果当前点 i 的排名或点权大于等于Key ,那么在分裂时 ii右子树上所有点的排名或点权显然都大于等于Key ,也就是都会被分入第二部分

而在考虑 i 剩下的另一棵子树时,显然对于另一棵子树的分裂也就和分裂 i 的过程一样。

于是我们就可以重复递归分裂直到以 i 为根的子树为空。

合并

合并操作也就是将两棵Treap合并成一棵Treap,其中一颗Treap的所有点点权均小于或大于另一颗Treap任意点点权。

显然两棵Treap都分别满足Treap的性质,并且Treap之间任意两点大小比较结果都一样。

我们假设第一棵根为 x 的Treap上所有点点权小于等于第二棵根为 y 的Treap上任意点点权,于是我们可以说第一棵Treap小于等于第二棵Treap。

由于二叉查找树的性质,显然对于每一个点,无论是比这个点点权大的值还是比这个点权小的值都能插入二叉查找树中,那么我们只需要将其中一棵Treap插入另一棵Treap中即可。

那么我们只需要考虑维护键值满足堆的性质。

显然由于 xy 大小关系,将 y 中部分插入 x 中部分时一定会插入 x 中与插入的Treap相接的节点的右子树,将 x 中部分插入 y 中部分时一定会插入 y 中与插入的Treap相接的节点的左子树

若点 x 键值 key_x 小于等于y 键值 key_y ,为满足堆的性质,此时需要将y 为根的Treap与点 x右儿子合并。

若点 x 键值 key_x 大于y 键值 key_y ,为满足堆的性质,此时需要将x 为根的Treap与点 y左儿子合并。

而将一棵Treap与另一棵Treap的子树合并的操作其实与将两棵Treap合并的操作无异。

那么我们同样可以反复递归处理到两棵Treap中任意一棵为空时停止合并。

插入

假设要插入的数为 a ,那么我们先将Treap裁成小于等于 ax 和大于 a y 两部分,然后我们将 ax 合并得到 x' ,然后我们再将 x'y 合并即可。

删除

我们先考虑如何从一颗所有点点权完全相同的Treap中删除一个值,显然我们只需要从中随便删除一个点即可。

而此时我们再考虑如何能让从中删除一个数的操作转换为以上问题,那么显然我们可以通过分离操作将所有小于要删除的数 a 的部分和所有大于 a 的部分裁出,然后对剩下的部分进行删除操作后再合并即可。

查询

显然查询 a 的排名的操作就和二叉查找树中情况无异。

而对于诸如查询前驱后继等操作,我们通过上面的分裂合并的思想来解决。

具体实现

//LiberOJ#104. 普通平衡树
#include<bits/stdc++.h>
using namespace std;
const int NR=100010,o=1e9+7;
int n,tot,tmp,Root,key[NR],val[NR],Size[NR],Tree[NR][2];
short Rand=1;
//val[i]表示编号为i的点的点权,key[i]表示编号为i的点的堆关键字,这里以不断乘(1e9+7)的Rand作为随机堆关键字
//Size[i]表示以编号为i的点为根的子树的节点个数,Tree[i][0]表示i左儿子,Tree[i][1]表示i右儿子
void Update(int id)//更新Size,id为当前点编号
{
    Size[id]=Size[Tree[id][0]]+Size[Tree[id][1]]+1;
}
void Create(int &id,int x)//创建新节点
{
    val[id=++tot]=x;//赋予编号及点权
    key[id]=Rand*=o;//随机赋予键值
    Size[id]=1;//初始化Size
}
int Merge(int id_x,int id_y)//合并,返回值为合并后的树的根节点
{
    if(!id_x||!id_y)//若子树x与子树y中任有一个为空,则就不需要继续合并下去(因为只剩一棵树或者一棵树都不剩了)
        return id_x+id_y;//返回合并后根节点
    if(key[id_x]<key[id_y]){//若子树x根节点键值小于子树y节点键值
        Tree[id_x][1]=Merge(Tree[id_x][1],id_y);//考虑将子树y并入以子树x右子树中
        Update(id_x);//更新Size
        return id_x;//返回合并后根节点
    }Tree[id_y][0]=Merge(id_x,Tree[id_y][0]);//考虑将子树x并入以子树y左子树中(注意此时xy在Merge中参数顺序不能颠倒)
    Update(id_y);//更新Size
    return id_y;//返回合并后根节点
}
void split(int id,int x,int &id_x,int &id_y)//分裂,id_x为分裂后子树x根节点,id_y为分裂后子树y根节点
{
    if(!id){//若当前节点为空
        id_x=id_y=0;//空树不用再分
        return ;//结束函数
    }
    if(val[id]<=x){//若关键值x大于等于当前点点权
        id_x=id;//当前点及其左子树并入子树x
        split(Tree[id][1],x,Tree[id][1],id_y);//继续分裂x右子树
    }else{//若关键值x小于当前点点权
        id_y=id;//当前点及其右子树并入子树y
        split(Tree[id][0],x,id_x,Tree[id][0]);//继续分裂左子树
    }Update(id);//更新Size
}
int Get(int id,int x)//获取排名为x的点
{
    if(x<=Size[Tree[id][0]])//若在左子树中(左子树节点数大于等于x)
        return Get(Tree[id][0],x);//向左子树搜索
    if(x==Size[Tree[id][0]]+1)//若在当前点(左子树节点数等于x-1)
        return id;//返回当前点
    return Get(Tree[id][1],x-Size[Tree[id][0]]-1);//若在右子树中,则考虑求右子树中排名为x减去左子树及当前点的节点总数的点
}
int main()
{
    scanf("%d",&n);
    int now,id_x,id_y;
    for(int i=1;i<=n;++i){
        int x,opt;
        scanf("%d%d",&opt,&x);
        if(opt==1){//插入值为x的数
            split(Root,x,id_x,id_y);//将小于等于x部分的子树和大于x部分的子树裁出
            Create(now,x);//新建点
            Root=Merge(Merge(id_x,now),id_y);//合并
        }
        if(opt==2){//删除值为x的数
            split(Root,x,id_x,tmp);//将小于等于x部分的子树和大于x部分的子树裁出
            split(id_x,x-1,id_x,id_y);//将小于等于x部分的子树再裁出小于x和等于x的两个子树
            id_y=Merge(Tree[id_y][0],Tree[id_y][1]);//将等于x的子树去掉其根节点(去掉一个x)
            Root=Merge(Merge(id_x,id_y),tmp);//然后再合并
        }
        if(opt==3){//查询x的排名
            split(Root,x-1,id_x,id_y);//将所有小于x的部分裁出
            printf("%d\n",Size[id_x]+1);//x排名即为小于x的数的数量(小于x部分的子树体积)加一
            Root=Merge(id_x,id_y);//再合并
        }
        if(opt==4)//查询排名为x的数
            printf("%d\n",val[Get(Root,x)]);//直接查询
        if(opt==5){//查询x前驱
            split(Root,x-1,id_x,id_y);//将所有小于x的部分裁出
            printf("%d\n",val[Get(id_x,Size[id_x])]);//排名为小于x的数的数量的数即为排名在x前一位的数,也就是x的前驱
            Root=Merge(id_x,id_y);//再合并
        }
        if(opt==6){//查询x后继
            split(Root,x,id_x,id_y);//将所有小于等于x的部分裁出
            printf("%d\n",val[Get(id_y,1)]);//在所有大于x的数中最小的数即为排名在x后一位的数,就是x的后继
            Root=Merge(id_x,id_y);//再合并
        }
    }
    return 0;
}

编辑于 2021-01-25 17:29