浅谈 Fhq-Treap

问题引入:Luogu P3369 普通平衡树

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入 x x x
  2. 删除 x x x 数(若有多个相同的数,因只删除一个)
  3. 查询 x x x 数的排名(排名定义为比当前数小的数的个数 + 1 +1 +1 )
  4. 查询排名为 x x x 的数
  5. x x x 的前驱(前驱定义为小于 x x x,且最大的数)
  6. x x x 的后继(后继定义为大于 x x x,且最小的数)

拿到这道题,你觉得暴力该怎么做?
v e c t o r vector vector 二分查找 + 暴力插入可以拿到 60 60 60 分,为什么会只有 60 60 60 呢?思考一下,我们的瓶颈在于插入是 O ( n ) O(n) O(n) 的,那么什么数据结构可以做到复杂度很低的插入呢? 那就是

考虑建立一颗二叉搜索树,对于任意节点 p p p 满足 v a l l c p < v a l p < v a l r c p val_{lc_p}<val_p<val_{rc_p} vallcp<valp<valrcp ,其中 l c p lc_p lcp r c p rc_p rcp 分别指 p p p 的左儿子和右儿子。

那么给出一组测试数据,看看建成的树会是什么样子:
在这里插入图片描述
给出操作 4 4 4 的代码:

inline int findxth(int root,int x)
{
	if(siz[L[root]]+1==x) return root;
	if(siz[L[root]]>=x) return findkth(L[root],x);
	return findkth(R[root],x-siz[L[root]]-1);
}

在代码中, s i z siz siz 树组保存的是子树的大小, L L L R R R 分别储存做儿子与右儿子的下标,所以当前数在子树中的排名是 s i z L r o o t + 1 siz_{L_{root}}+1 sizLroot+1,那么分类讨论:

  1. x ≤ s i z L r o o t x\leq siz_{L_{root}} xsizLroot,那么以 r o o t root root 为根的的子树中,第 x x x 大是其左子树上的第 x x x 大;
  2. x = s i z L r o o t + 1 x=siz_{L_{root}}+1 x=sizLroot+1,那么以 r o o t root root 为根的的子树中,第 x x x 大就是 r o o t root root
  3. x > s i z L r o o t + 1 x> siz_{L_{root}}+1 x>sizLroot+1,那么那么以 r o o t root root 为根的的子树中,第 x x x 大是其左子树上的第 x − s i z L r o o t − 1 x-siz_{L_{root}}-1 xsizLroot1 大。

思考一下为什么最后是 x − s i z L r o o t − 1 x-siz_{L_{root}}-1 xsizLroot1:其实是因为前 s i z L r o o t + 1 siz_{L_{root}}+1 sizLroot+1 个都在左子树与根上。

那么怎么卡掉这种数据结构呢?

每次插入数单调递增或递减,那么就会退化成这样的链:
在这里插入图片描述
那么复杂度就退化为 O ( n ) O(n) O(n) 的了,还不如写个暴力。

那我们怎么解决这种情况呢? 我们需要代入堆的性质:
对于一个堆,根节点的权一定小于任意儿子的权。

怎么把这个性质代入二叉搜索树中呢?对于普通的 T r e a p Treap Treap ,我们引入两种旋转操作,左旋与右旋,每个节点赋予一个随机的权,通过不断的左旋与右旋,满足其堆的性质,并且保留其二叉搜索树的性质,那么这棵树是期待平衡的。

但在这片文章中,我们不讨论左旋与右旋,运用大佬 F h q Fhq Fhq 自创的 F h q − T r e a p Fhq-Treap FhqTreap ,来维护其堆的性质。

先来看其核心操作:分裂与合并:

Split 分裂

分裂有两种,分别为 按值分裂按排名分裂

按值分裂

按值分裂指把一颗树按给定的权值 p o s pos pos 分裂成两颗子树,我们称为 左树右树,满足:左树上的权值都小于等于 p o s pos pos ,右树上的权值大于 p o s pos pos,那么怎么处理呢?

比如在当前树中:
在这里插入图片描述

我们按 p o s = 15 pos=15 pos=15 分割:
首先,这个东西肯定是递归的,我们先尝试对根处理:
24 > 15 24>15 24>15 ,那么它一定属于右树的:
在这里插入图片描述

我们突然现,因为 24 > 15 24>15 24>15 ,由于二叉搜索树的性质,右儿子的权值总是比根大,那么 24 24 24 的右子树上的所有权值一定是大于 15 15 15 的,那么它们都属于右子树的:

在这里插入图片描述
我们现在只需要对 24 24 24 的左子树进行操作:

在这里插入图片描述
发现 13 < 15 13<15 13<15,那么 13 13 13 的左子树上的权值也一定小于 15 15 15,那么分裂树变成这样:
在这里插入图片描述
13 13 13 的右子树,即 14 14 14 进行判断,那么分裂结果如下:
在这里插入图片描述
给出递归代码:

inline void split(int p,int v,int &l,int &r){
	if(!p) {l=r=0;return;}
	//没有了,返回
	else if(val[p]<=v) l=p,split(R[p],v,R[p],r);
	//当前节点属于左树,那么左子树也一定属于左树,只需在右子树中再递归判断
	else r=p,split(L[p],v,l,L[p]);
	//同理
	upd(p);
}

此处 l , r l,r l,r 是引用的,那么只需在外定义两个整数,在分裂时当做参数传入,执行完 l , r l,r l,r 即为左右树的根。
F h q − t r e a p Fhq-treap Fhqtreap 的好处在于你只需要想好一半,另一半只需要复制粘贴再改为相反的即可。

注: u p d upd upd 函数是用来维护子树大小的,给出代码:

inline void upd(int p)
{
	siz[p]=siz[L[p]]+siz[R[p]]+1;
}
按排名分裂

给出代码,原理同上给出操作 4 4 4 的代码,自行思考:

inline void split(int p, int k, int &x, int &y)
{
	if(!p){x = y = 0;return;}
	else if(r <= siz[L[p]]) y=p,split(L[p],k,x,L[p]);
	else x=id,split(R[p],k-siz[L[p]]-1,R[p],y);
	upd(p);
}

那么 F h q − T r e a p Fhq-Treap FhqTreap 核心代码已经写完,接下来切题

Luogu P3369 普通平衡树:

  1. 插入一个数 x x x
inline void ins(int a)
{
	split(rt,a,x,y);
	cre(tmp,a);
	rt=merge(merge(x,tmp),y);
}

按值 a a a 分裂为两个树,创立新节点,节点序号为 t m p tmp tmp(此处为引用),值为 a a a
给出 c r e cre cre 创建节点代码:

inline void cre(int &p,int v){
	val[++tot]=v;
	siz[tot]=1;
	rand(rd[tot]);
	p=tot;
}

t o t tot tot 为当前的节点数, r a n d rand rand 中的参数为引用的,即为给其赋予一个伪随机值,给出 r a n d rand rand 函数代码:

int rnd=1345252;
void rand(int &x){x=(rnd=(rnd*123044)%1000000000);}
  1. 删除一个数 x x x
inline void del(int a){
	split(rt,a,x,y);
	split(x,a-1,x,z);
	z=merge(L[z],R[z]);
	rt=merge(merge(x,z),y);
}

经过两次分裂, z z z 树上所有节点权值都为 x x x,因为只需要删除一个,那么考虑删除根,合并左右节点,记得再合并回去,此处 r t rt rt 为整树的根

  1. 查询 x x x 数的排名
inline int findrank(int a){
	split(rt,a-1,x,y);
	tmp=siz[x]+1;
	rt=merge(x,y);
	return tmp;
}

分裂后左树上所有节点权小于 x x x ,右树上所有节点权值大于等于 x x x ,那么 x x x 的排名即为左树大小 + 1 +1 +1

  1. 与二叉搜索树原理相同
inline int findkth(int p,int k){
	if(siz[L[p]]+1==k) return p;
	if(siz[L[p]]>=k) return findkth(L[p],k);
	return findkth(R[p],k-siz[L[p]]-1);
}
  1. x x x 数的前驱
inline int front(int a){
	split(rt,a-1,x,y);
	tmp=findkth(x,siz[x]);
	rt=merge(x,y);
	return tmp;
}

即为分裂后左树(左树权值小于 x x x) 中,排名为左树大小的权值

  1. x x x 数的后继
inline int back(int a){
	split(rt,a,x,y);
	tmp=findkth(y,1);
	rt=merge(x,y);
	return tmp;
}

思路与 5 5 5 无异

注意:分裂后一定要合并回来

给出完整代码:

#include <stdio.h>
#define Maxn 200004
int L[Maxn],R[Maxn];
int val[Maxn],siz[Maxn],rd[Maxn],tmp;
int rt,tot,x,y,z,rnd=1345252;
void rand(int &x){x=(rnd=(rnd*123044)%1000000000);}
inline void upd(int p){
	siz[p]=siz[L[p]]+siz[R[p]]+1;
}
inline void cre(int &p,int v){
	val[++tot]=v;
	siz[tot]=1;
	rand(rd[tot]);
	p=tot;
}
inline void split(int p,int v,int &l,int &r){
	if(!p) {l=r=0;return;}
	else if(val[p]<=v) l=p,split(R[p],v,R[p],r);
	else r=p,split(L[p],v,l,L[p]);
	upd(p);
}
inline int merge(int l,int r){
	if(!l||!r) return l+r;
	if(rd[l]<rd[r]){
		R[l]=merge(R[l],r);upd(l);
		return l;
	}
	L[r]=merge(l,L[r]);	upd(r);
	return r;
}
inline void ins(int a){
	split(rt,a,x,y);
	cre(tmp,a);
	rt=merge(merge(x,tmp),y);
}
inline void del(int a){
	split(rt,a,x,y);
	split(x,a-1,x,z);
	z=merge(L[z],R[z]);
	rt=merge(merge(x,z),y);
}
inline int findrank(int a){
	split(rt,a-1,x,y);
	tmp=siz[x]+1;
	rt=merge(x,y);
	return tmp;
}
inline int findkth(int p,int k){
	if(siz[L[p]]+1==k) return p;
	if(siz[L[p]]>=k) return findkth(L[p],k);
	return findkth(R[p],k-siz[L[p]]-1);
}
inline int front(int a){
	split(rt,a-1,x,y);
	tmp=findkth(x,siz[x]);
	rt=merge(x,y);
	return tmp;
}
inline int back(int a){
	split(rt,a,x,y);
	tmp=findkth(y,1);
	rt=merge(x,y);
	return tmp;
}
int main(){
	int Q,opt,a;
	scanf("%d",&Q);
	while(Q--)
	{
		scanf("%d%d",&opt,&a);
		if(opt==1) ins(a);
		else if(opt==2) del(a);
		else if(opt==3) printf("%d\n",findrank(a));
		else if(opt==4) printf("%d\n",val[findkth(rt,a)]);
		else if(opt==5) printf("%d\n",val[front(a)]);
		else printf("%d\n",val[back(a)]);
	}
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值