FHQ Treap入门教程(含洛谷P3369 & LOJ#104 普通平衡树题解qwq)

前置技能

二叉搜索树

前置技能有旋转treap?不存在的(我到现在仍然不会旋转的treap qwq)

Treap简介

引用维基百科上一句精辟的话:Treap=Tree+Heap
在Treap上需要维护两个值:一个优先级pripri,一个节点权值valval
其中优先级取随机数,满足小根堆的性质。节点权值满足二叉搜索树的性质。
即每个节点的pripri值均小于左、右孩子的pripri值,每个节点的左子树的valval均小于这个节点的valval值,右子树的valval大于这个节点的valval值。
示例:
在这里插入图片描述
显然,Treap的每棵子树都是一棵Treap。
珂以证明(虽然我不会证qwq),Treap的树高为lognlogn
普通的Treap是通过旋转来维护其性质的,而FHQ Treap通过合并(Merge)和分裂(Split)来维护qwq。

Merge操作

比如要合并两棵分别以x,yx,y为根的树,假设xxvalval值<=yyvalval值。
然后按优先级决定谁是根qwq,当xxpripri值比yypripri值小时,xx为新的根,否则yy为新的根,这样珂以使pripri仍然满足小根堆的性质。
xxpripri值更小时,xxvalval值小于yyvalval值,根据二叉搜索树的性质,应该把yy放到右子树中。因此把xx的右孩子与yy合并。
yypripri值更小时,xxvalval值小于yyvalval值,所以应该把xx放到左子树中,因此把yy的左孩子与xx合并。
最后需要更新根节点的sizsiz值qwq(即子树内的节点个数)

Merge 的毒瘤代码:

inline void Update(int x) { //更新子树大小 
    tree[x].siz=tree[lc(x)].siz+tree[rc(x)].siz+1;
}
int Merge(int x,int y) {
    //合并以x和y为根的树 
    if(!x || !y)    return x+y;
    //用优先级决定新的根是x还是y 
    if(tree[x].pri<tree[y].pri) {
		rc(x)=Merge(rc(x),y);
        Update(x);
        return x;
    } else {
		lc(y)=Merge(x,lc(y));
        Update(y);
        return y;
    }
}

Split操作

这里讲述的是把一棵树按权值分为两棵子树的方法。
假设要把ii的子树中权值&lt;=k&lt;=k的分到以xx为根的Treap中,剩下的权值&gt;k&gt;k的分到以yy为根的Treap中。
ii的权值&lt;=k&lt;=k时,根据二叉搜索树的性质,ii的左子树中的所有节点权值均&lt;=k&lt;=k
所以把iiii的左子树内的所有节点都分给xx,然后递归把ii的右子树中&lt;=k&lt;=k的分到xx的右子树(这样仍然满足valval为二叉搜索树的性质),ii的右子树中&gt;k&gt;k的分给yy即可。
ii的权值&gt;k&gt;k时,ii的右子树中的所有节点权值均&gt;k&gt;k
同理,把iiii右子树内的所有节点均分给yy,然后递归把ii的左子树中的&gt;k&gt;k的分到yy的左子树,ii的左子树中&lt;=k&lt;=k的分给xx即可qwq。

Split 的毒瘤代码:

void Split(int i,int k,int &x,int &y) {
    //把以i为根的树分成以x为根和以y为根的树 
    //把所有权值<=k的分到x中,>k的分到y中 
    if(!i) {
        //若i为空节点,则x和y也为空 
        x=y=0;
    } else {
        if(tree[i].val<=k) {
        	//这里先让x=i,然后递归下去就把i的右子树中<=k的分到了rc(x) 
            x=i;
            Split(rc(i),k,rc(x),y);
        } else {
            //同理 
            y=i;
            Split(lc(i),k,x,lc(y));
        }
        Update(i);	//记得更新子树大小 
    }
}

找权值第k大

因为Treap的valval满足二叉搜索树性质,所以类似地按照二叉搜索树的方法找即可。
比较巧妙的一点是:tree[lc(i)].siz+1tree[lc(i)].siz+1表示左子树加上根这个节点的总节点数qwq。
代码:

int kth(int i,int k) {
    //找到i的子树中val第k大的点的下标 
    while(true) {
        if(k<=tree[lc(i)].siz) {
            //若k比左子树大小还小 
            //则第k大显然在左子树中 
            i=lc(i);
        } else if(k>tree[lc(i)].siz+1) {
            //同理 
            k-=tree[lc(i)].siz+1;
            i=rc(i);
        } else {
            return i;
        }
    }
}

另外放一个新建节点的New函数代码:

inline int New(int v) {   //新建节点并返回其下标 
    tree[++tot].val=v;
    tree[tot].pri=rand();
    tree[tot].siz=1;
    return tot;
}

例题

洛谷P3369 【模板】普通平衡树
LOJ #104 普通平衡树

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1.插入xx
2.删除xx数(若有多个相同的数,因只删除一个)
3.查询xx数的排名(排名定义为比当前数小的数的个数+1。若有多个相同的数,因输出最小的排名)
4.查询排名为xx的数
5.求xx的前驱(前驱定义为小于xx,且最大的数)
6.求xx的后继(后继定义为大于xx,且最小的数)

这里讲一下操作1和操作2,其他操作比较简单,看代码注释就珂以了qwq(我不会告诉你只是我懒得写qwq

操作1:

因为Merge操作是需要xx的权值&lt;=y&lt;=y的权值,所以不能直接New一个节点然后直接大莉Merge qwq。
假设当前需要插入的节点权值为num,我们考虑把这棵Treap按照权值分为两棵树,权值&lt;=num&lt;=num的分到xx&gt;num&gt;num的分到yy
所以珂以把xx和New得到的节点合并,再把得到的结果与yy合并。

操作2:

假设要删掉一个权值为numnum的节点。
这里珂以考虑把所有权值为numnum的节点分到一棵树中,然后删根节点。
具体实现方法:把所有权值&lt;=num&lt;=num的节点分到xx,权值&gt;num&gt;num的分到zz
然后把z\color{red}z中权值&lt;=num1&lt;=num-1的分到xx,权值&gt;=num&gt;=num的分到yy
不难发现这样yy中存的都是权值为numnum的点,此时把根节点删掉即可qwq

例题代码

#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<vector>
#define re register int
#define lc(x) tree[x].ls
#define rc(x) tree[x].rs
using namespace std;
typedef long long ll;
int read() {
    re x=0,f=1;
    char ch=getchar();
    while(ch<'0' || ch>'9') {
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9') {
        x=10*x+ch-'0';
        ch=getchar();
    }
    return x*f;
}
const int Size=200005;
struct node {
    int ls,rs;  //左右孩子 
    int val;    //权值,左<根<右 
    int pri;    //优先级,维护小根堆 
    int siz;    //子树大小 
} tree[Size];
int n,tot;
inline void Update(int x) { //更新子树大小 
    tree[x].siz=tree[lc(x)].siz+tree[rc(x)].siz+1;
}
inline int New(int v) {   //新建节点并返回其下标 
    tree[++tot].val=v;
    tree[tot].pri=rand();
    tree[tot].siz=1;
    return tot;
}
int Merge(int x,int y) {
    //合并以x和y为根的树 
    if(!x || !y)    return x+y;
    //用优先级决定新的根是x还是y 
    if(tree[x].pri<tree[y].pri) {
		rc(x)=Merge(rc(x),y);
        Update(x);
        return x;
    } else {
		lc(y)=Merge(x,lc(y));
        Update(y);
        return y;
    }
}
void Split(int i,int k,int &x,int &y) {
    //把以i为根的树分成以x为根和以y为根的树 
    //把所有权值<=k的分到x中,>k的分到y中 
    if(!i) {
        //若i为空节点,则x和y也为空 
        x=y=0;
    } else {
        if(tree[i].val<=k) {
        	//这里先让x=i,然后递归下去就把i的右子树中<=k的分到了rc(x) 
            x=i;
            Split(rc(i),k,rc(x),y);
        } else {
            //同理 
            y=i;
            Split(lc(i),k,x,lc(y));
        }
        Update(i);	//记得更新子树大小 
    }
}
int kth(int i,int k) {
    //找到i的子树中val第k大的点的下标 
    while(true) {
        if(k<=tree[lc(i)].siz) {
            //若k比左子树大小还小 
            //则第k大显然在左子树中 
            i=lc(i);
        } else if(k>tree[lc(i)].siz+1) {
            //同理 
            k-=tree[lc(i)].siz+1;
            i=rc(i);
        } else {
            return i;
        }
    }
}
inline int Get_K(int rt,int rk) {
    //得到第k大的点的权值 
    return tree[kth(rt,rk)].val;
}
int main() {
    //暴力**不可避 
    srand(19260817);
    n=read();
    int root=0;
    int x=0,y=0,z=0;
    while(n--) {
        int op=read();
        int num=read();
        if(op==1) {
            //插入num 
            Split(root,num,x,y);
            root=Merge(Merge(x,New(num)),y);
        } else if(op==2) {
            //删除num 
            Split(root,num,x,z);
            //此时x中存了所有权值<=num的点 
            Split(x,num-1,x,y);
            //此时x中存了所有权值<=num-1的点 
            //因此y中存了所有权值为num的点 
            //然后把y的根节点删掉 
            y=Merge(lc(y),rc(y));
            root=Merge(Merge(x,y),z);
        } else if(op==3) {
            //查询num的排名 
            //x中存的是所有权值<=num-1的点 
            Split(root,num-1,x,y);
            //因此x的大小+1就是num的排名 
            printf("%d\n",tree[x].siz+1);
            root=Merge(x,y);
        } else if(op==4) {
            //查询第num小的数 
            printf("%d\n",Get_K(root,num));
        } else if(op==5) {
            //查询num的前驱 
            //x存的是所有权值<=num-1的点 
            //x中第(x子树大小)小的点就是num前驱
            Split(root,num-1,x,y);
            printf("%d\n",Get_K(x,tree[x].siz));
            root=Merge(x,y);
        } else {
            //x的后继(基本同理) 
            //y存的是所有权值>num的点 
            //因此y中第一小的就是num前驱 
            Split(root,num,x,y);
            printf("%d\n",Get_K(y,1));
            root=Merge(x,y);
        }
    }
    return 0;
}

其他题

bzoj3173 & 洛谷P4309 [TJOI2013]最长上升子序列

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章