Treap

本文详细介绍了Treap数据结构的性质、复杂度、旋转操作,并提供了基本代码实现,包括节点的添加、删除和旋转。此外,还讨论了Treap在解决实际问题中的应用,如查找前驱和后继节点,以及在洛谷上的例题解析和练习题目推荐。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Treap思路

Treap性质

建一棵二叉搜索树,每个节点除了有一个数值外,还需要赋予一个随机值 r d rd rd。这一棵二叉搜索树同时满足两个条件:

  • 满足二叉搜索树的基本性质:一个节点的左儿子的值小于他,一个节点的右儿子的值大于它。
  • 赋予的随机值满足小顶堆的基本性质。

Treap复杂度

事实上,Treap至今还没有得到严谨的证明时间复杂度为 Θ ( n log ⁡ n ) \Theta(n\log n) Θ(nlogn),但是只要这个随机生成的序列不是基本严格递增的,那么这一棵Treap就一定不会退化为 Θ ( n 2 ) \Theta(n^2) Θ(n2),接近于 Θ ( n log ⁡ n ) \Theta(n\log n) Θ(nlogn)。虽然Treap的时间复杂度没有Splay树等稳定,但一般也不会卡点测试你的代码,srand成系统时间即可。

Treap旋转

改变Treap的形态但是不改变Treap性质的过程称之为旋转。Treap的旋转定义与Splay树有所不同。Treap中,左旋指把根节点放到左边,右旋指把根节点放到右边,可形象理解为左旋即为把整棵树向左边掰一个节点,右旋即为把整棵树向右边掰一个节点。

左旋右旋示意图
思路基本介绍完毕,接下来讲代码。

Treap基本代码

所需变量

开始前,需要定义两个基本变量: r t , t o t rt,tot rt,tot,其中, r t rt rt表示该Treap的根节点是谁,而 t o t tot tot表示一共有多少个节点。

对于树中的每一个节点,需要有这样几个变量: l , r , s z , c n t , k e y , r d l,r,sz,cnt,key,rd l,r,sz,cnt,key,rd l , r l,r l,r分别表示这个节点的左儿子与右儿子分别是谁, s z sz sz表示以这个点为根节点的二叉树的所有节点数量是多少, c n t cnt cnt表示与该节点相等的数有多少个(如果值相等,那么直接将该节点的 c n t cnt cnt++,但是不加入新的节点), k e y key key表示数值, r d rd rd表示赋予该节点的随机数。

c o d e : \tt{code:} code:

struct node{
	int l,r,sz,cnt,key,rd;
}tr[100010];
int rt=0,tot=0;

加入节点

每次新加入节点时,需要通过点权( k e y key key值)大小找到该数值在Treap中的正确位置,将这个节点的 l , r l,r l,r都设为 0 0 0,将 s z , c n t sz,cnt sz,cnt都设为 1 1 1(都只有该节点自己), k e y key key值即为数值, r d rd rd值即为随机生成值。

在加入节点完毕后,回溯的时候需要将整一棵树左旋或右旋,保证平衡性。

c o d e : \tt{code:} code:

void insert(int &nd,int x){//插入
	if(nd==0){
		nd=++tot;
		tr[nd].key=x;
		tr[nd].rd=rand();
		tr[nd].cnt=tr[nd].sz=1;
		return;
	}
	tr[nd].sz++;
	if(x==tr[nd].key){
		tr[nd].cnt++;
	}
	else if(x<tr[nd].key){
		insert(tr[nd].l,x);
		if(tr[nd].rd>tr[tr[nd].l].rd){
			Rr(nd);
		}
	}
	else{
		insert(tr[nd].r,x);
		if(tr[nd].rd>tr[tr[nd].r].rd){
			Lr(nd);
		}
	}
}

删除节点

首先找到该数值的位置,然后将该数值的节点的 r d rd rd值改为 i n f inf inf,然后不断按照规则左旋或者右旋。查找位置过程中不断维护树的 c n t , s z cnt,sz cnt,sz值,时时更新。最后旋完以后,要删除的节点一定会在整个Treap的最右边,最下方。将那个点象征性删除即可。

c o d e : \tt{code:} code:

void DEL(int &nd){//服务del
	if(tr[nd].l==0||tr[nd].r==0){
		nd=tr[nd].l+tr[nd].r;
		return;
	}
	if(tr[tr[nd].l].rd<tr[tr[nd].r].rd){
		Rr(nd);
		DEL(tr[nd].r);
	}
	else{
		Lr(nd);
		DEL(tr[nd].l);
	}
}
void del(int &nd,int x){//删除
	if(nd==0){
		return;
	}
	if(tr[nd].key==x){
		tr[nd].sz--;
		tr[nd].cnt--;
		if(tr[nd].cnt==0){
			DEL(nd);
		}
	}
	else if(x<tr[nd].key){
		del(tr[nd].l,x);
	}
	else if(x>tr[nd].key){
		del(tr[nd].r,x);
	}
	tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
}

其中第一个DEL函数是为del函数服务的,调用时直接调用del即可。

旋转

左旋与右旋原理基本相同,以右旋为例:
左旋右旋示意图
将根节点的左儿子定为根节点,将自己定位新的根节点的右儿子,将新的根节点的左儿子不变,新的根节点的右儿子变为原来根节点的左儿子(那个位置必定为空位)。

c o d e : \tt{code:} code:

void Rr(int &nd){//右旋
	int w=tr[nd].l;
	tr[nd].l=tr[w].r;
	tr[w].r=nd;
	tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
	tr[w].sz=tr[tr[w].l].sz+tr[tr[w].r].sz+tr[w].cnt;
	nd=w;
}
void Lr(int &nd){//左旋
	int w=tr[nd].r;
	tr[nd].r=tr[w].l;
	tr[w].l=nd;
	tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
	tr[w].sz=tr[tr[w].l].sz+tr[tr[w].r].sz+tr[w].cnt;
	nd=w;
}

前驱与后继

思路简单,以求前驱为例:

如果该节点是没有的,那么直接返回无穷大;如果该节点的值小于要找的值,那么返回该位置的值与右子树求该数前驱的较大值;如果该节点的值大于要找的值,那么返回从他的左子树找到的要找的数的前驱的值。思路描述有些拗口,需仔细理解。后继的求法与前驱类似,代码也类似

c o d e : \tt{code:} code:

int pre(int nd,int x){//前驱
	if(nd==0){
		return INT_MIN;
	}
	if(tr[nd].key<x){
		return max(tr[nd].key,pre(tr[nd].r,x));
	}
	else{
		return pre(tr[nd].l,x);
	}
}
int succ(int nd,int x){//后继
	if(nd==0){
		return INT_MAX;
	}
	if(tr[nd].key>x){
		return min(tr[nd].key,succ(tr[nd].l,x));
	}
	else{
		return succ(tr[nd].r,x);
	}
}

查询

查询分为两种:查询数 x x x的排名与查询排名为 x x x的数。思路看代码应该就能看懂(个人认为应该属于Treap的衍生能力,不属于基本能力)。思路不再赘述,只是一堆if

c o d e : \tt{code:} code:

int pm(int nd,int x){//数x的排名
	if(nd==0){
		return INT_MIN;
	}
	if(tr[nd].key==x){
		return tr[tr[nd].l].sz;
	}
	if(tr[nd].key>x){
		return pm(tr[nd].l,x);
	}
	else{
		return tr[tr[nd].l].sz+tr[nd].cnt+pm(tr[nd].r,x);
	}
}
int rk(int nd,int x){//排名为x的数
	if(tr[tr[nd].l].sz>=x){
		return rk(tr[nd].l,x);
	}
	if(tr[tr[nd].l].sz+tr[nd].cnt>=x){
		return tr[nd].key;
	}
	return rk(tr[nd].r,x-tr[tr[nd].l].sz-tr[nd].cnt);
}

Treap例题

题目

洛谷P3369

思路

思路已经全部在上面介绍,这里不再赘述,Treap模板题。

代码

声明:仅供参考。

#include<bits/stdc++.h>
using namespace std;
struct node{
	int l,r,sz,cnt,key,rd;
}tr[100010];
int rt=0,tot=0;
void Rr(int &nd){
	int w=tr[nd].l;
	tr[nd].l=tr[w].r;
	tr[w].r=nd;
	tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
	tr[w].sz=tr[tr[w].l].sz+tr[tr[w].r].sz+tr[w].cnt;
	nd=w;
}
void Lr(int &nd){
	int w=tr[nd].r;
	tr[nd].r=tr[w].l;
	tr[w].l=nd;
	tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
	tr[w].sz=tr[tr[w].l].sz+tr[tr[w].r].sz+tr[w].cnt;
	nd=w;
}
void DEL(int &nd){
	if(tr[nd].l==0||tr[nd].r==0){
		nd=tr[nd].l+tr[nd].r;
		return;
	}
	if(tr[tr[nd].l].rd<tr[tr[nd].r].rd){
		Rr(nd);
		DEL(tr[nd].r);
	}
	else{
		Lr(nd);
		DEL(tr[nd].l);
	}
}
void del(int &nd,int x){
	if(nd==0){
		return;
	}
	if(tr[nd].key==x){
		tr[nd].sz--;
		tr[nd].cnt--;
		if(tr[nd].cnt==0){
			DEL(nd);
		}
	}
	else if(x<tr[nd].key){
		del(tr[nd].l,x);
	}
	else if(x>tr[nd].key){
		del(tr[nd].r,x);
	}
	tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
}
void insert(int &nd,int x){
	if(nd==0){
		nd=++tot;
		tr[nd].key=x;
		tr[nd].rd=rand();
		tr[nd].cnt=tr[nd].sz=1;
		return;
	}
	tr[nd].sz++;
	if(x==tr[nd].key){
		tr[nd].cnt++;
	}
	else if(x<tr[nd].key){
		insert(tr[nd].l,x);
		if(tr[nd].rd>tr[tr[nd].l].rd){
			Rr(nd);
		}
	}
	else{
		insert(tr[nd].r,x);
		if(tr[nd].rd>tr[tr[nd].r].rd){
			Lr(nd);
		}
	}
}
int pre(int nd,int x){
	if(nd==0){
		return INT_MIN;
	}
	if(tr[nd].key<x){
		return max(tr[nd].key,pre(tr[nd].r,x));
	}
	else{
		return pre(tr[nd].l,x);
	}
}
int succ(int nd,int x){
	if(nd==0){
		return INT_MAX;
	}
	if(tr[nd].key>x){
		return min(tr[nd].key,succ(tr[nd].l,x));
	}
	else{
		return succ(tr[nd].r,x);
	}
}
int pm(int nd,int x){
	if(nd==0){
		return INT_MIN;
	}
	if(tr[nd].key==x){
		return tr[tr[nd].l].sz;
	}
	if(tr[nd].key>x){
		return pm(tr[nd].l,x);
	}
	else{
		return tr[tr[nd].l].sz+tr[nd].cnt+pm(tr[nd].r,x);
	}
}
int rk(int nd,int x){
	if(tr[tr[nd].l].sz>=x){
		return rk(tr[nd].l,x);
	}
	if(tr[tr[nd].l].sz+tr[nd].cnt>=x){
		return tr[nd].key;
	}
	return rk(tr[nd].r,x-tr[tr[nd].l].sz-tr[nd].cnt);
}
int main(){
	int m,op,x;
	srand(time(0));
	cin>>m;
	while(m--){
		cin>>op;
		cin>>x;
		int t;
		switch(op){
			case 1:insert(rt,x);break;
			case 2:del(rt,x);break;
			case 3:t=pm(rt,x);cout<<(t<0?-1:t+1)<<endl;break;
			case 4:if(tr[rt].sz<x)cout<<-1<<endl;else cout<<rk(rt,x)<<endl;break;
			case 5:cout<<pre(rt,x)<<endl;break;
			case 6:cout<<succ(rt,x)<<endl;break;
		}
	}
	return 0;
}

Treap练习

推荐几道洛谷的高质量Treap题目,供练习。

鬼子进村

洛谷P1503

送花

洛谷P2073

全部是Treap模板,思路简单。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值