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

前置技能

二叉搜索树

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

Treap简介

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

Merge操作

比如要合并两棵分别以 x , y x,y x,y为根的树,假设 x x x v a l val val值<= y y y v a l val val值。
然后按优先级决定谁是根qwq,当 x x x p r i pri pri值比 y y y p r i pri pri值小时, x x x为新的根,否则 y y y为新的根,这样珂以使 p r i pri pri仍然满足小根堆的性质。
x x x p r i pri pri值更小时, x x x v a l val val值小于 y y y v a l val val值,根据二叉搜索树的性质,应该把 y y y放到右子树中。因此把 x x x的右孩子与 y y y合并。
y y y p r i pri pri值更小时, x x x v a l val val值小于 y y y v a l val val值,所以应该把 x x x放到左子树中,因此把 y y y的左孩子与 x x x合并。
最后需要更新根节点的 s i z siz siz值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操作

这里讲述的是把一棵树按权值分为两棵子树的方法。
假设要把 i i i的子树中权值 &lt; = k &lt;=k <=k的分到以 x x x为根的Treap中,剩下的权值 &gt; k &gt;k >k的分到以 y y y为根的Treap中。
i i i的权值 &lt; = k &lt;=k <=k时,根据二叉搜索树的性质, i i i的左子树中的所有节点权值均 &lt; = k &lt;=k <=k
所以把 i i i i i i的左子树内的所有节点都分给 x x x,然后递归把 i i i的右子树中 &lt; = k &lt;=k <=k的分到 x x x的右子树(这样仍然满足 v a l val val为二叉搜索树的性质), i i i的右子树中 &gt; k &gt;k >k的分给 y y y即可。
i i i的权值 &gt; k &gt;k >k时, i i i的右子树中的所有节点权值均 &gt; k &gt;k >k
同理,把 i i i i i i右子树内的所有节点均分给 y y y,然后递归把 i i i的左子树中的 &gt; k &gt;k >k的分到 y y y的左子树, i i i的左子树中 &lt; = k &lt;=k <=k的分给 x x x即可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的 v a l val val满足二叉搜索树性质,所以类似地按照二叉搜索树的方法找即可。
比较巧妙的一点是: t r e e [ l c ( i ) ] . s i z + 1 tree[lc(i)].siz+1 tree[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.插入 x x x
2.删除 x x x数(若有多个相同的数,因只删除一个)
3.查询 x x x数的排名(排名定义为比当前数小的数的个数+1。若有多个相同的数,因输出最小的排名)
4.查询排名为 x x x的数
5.求 x x x的前驱(前驱定义为小于 x x x,且最大的数)
6.求 x x x的后继(后继定义为大于 x x x,且最小的数)

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

操作1:

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

操作2:

假设要删掉一个权值为 n u m num num的节点。
这里珂以考虑把所有权值为 n u m num num的节点分到一棵树中,然后删根节点。
具体实现方法:把所有权值 &lt; = n u m &lt;=num <=num的节点分到 x x x,权值 &gt; n u m &gt;num >num的分到 z z z
然后把 z 中 \color{red}z中 z权值 &lt; = n u m − 1 &lt;=num-1 <=num1的分到 x x x,权值 &gt; = n u m &gt;=num >=num的分到 y y y
不难发现这样 y y y中存的都是权值为 n u m num num的点,此时把根节点删掉即可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]最长上升子序列

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值