非旋Treap学习

非旋Treap

好东西!!!从疯狂‘赚’的Treap到不‘赚’,Treap付出了时间的代价,但减少了代码的长度多么重要的一点呀!!!感谢神FHQ(orz)发明了这个数据结构。看过冗长的Treap后,再来打非旋Treap,是如此的释然。非旋Treap最大的优点就是代码短,方便实现,支持操作多。今天,来讲讲一种二分查找树非旋Treap。

非旋Treap最重要的操作是split(分裂)与merge(合并),其他的操作都是围绕着它展开的。

接下来,我们来讨论一下它的各项操作。


前导--笛卡尔树

一种建树方法

只是介绍一下。

  • 笛卡尔树是一棵二叉树,树的每个节点有两个值,一个为 key,一个为 val。
  • 光看 key 的话,笛卡尔树是一棵二叉搜索树,每个节点的左子树的 key 都比它 小,右子树都比它大;
  • 光看 val 的话,笛卡尔树有点类似堆,根节点的 val 是最小(或者最大)的,每个节点的 val 都比它的子树要大。
  • 笛卡尔树构造是和 Treap 完全一样的,如果 key 值是有序的,那么笛卡尔树的 构造是线性的,所以我们只要把 Treap 当作一颗笛卡尔树构造就可以了。

非旋Treap属于笛卡尔树,是通过笛卡尔树构造的。


非旋Treap

构造

笔者用的是指针。

struct Treap
{
	Treap* ch[2];
	int siz,rnd,val;
	void updata(){
		siz=1+ch[0]->siz+ch[1]->siz;
	}
};
Treap* null=new Treap();
Treap* rt=null;
typedef pair<Treap*,Treap*> ptt;

分裂

无旋Treap的重点就在于它的有序性,它可以在找到操作位置后,将这棵树分裂成两棵有序的子树。因为非旋Treap具有二叉树搜索树的性质,我们可以通过对其儿子大小的判断,找到最合适的拆分点,对其进行拆分。

ptt split(Treap* p,int x)//分裂 
{
	if(p==null) 
		return ptt(null,null);
	ptt y;
	if(p->ch[0]->siz>=x){
		y=split(p->ch[0],x);
		p->ch[0]=y.second;
		p->updata();
		y.second=p;
		return y;
	}
	y=split(p->ch[1],x-1-p->ch[0]->siz);
	p->ch[1]=y.first;p->updata();y.first=p;
	return y;
}

合并

在加入子树之后,我们会具有多棵树,此时,我们需要将它们合并在一起,所以,就出现了合并的操作,这样我们就可在过程中顺便维护一下非旋Treap的性质。注意在合并的过程中,要注意左右子树的有序性。

Treap* merge(Treap* a,Treap* b)//合并 
{
	if(a==null) return b;
	if(b==null) return a;
	if(a->rnd<b->rnd){
		a->ch[1]=merge(a->ch[1],b);
		a->updata();
		return a;
	}
	b->ch[0]=merge(a,b->ch[0]);
	b->updata();
	return b;
}

再有了这些操作后,我们就有了下面的操作。

加点

通过分裂与合并,我们很轻松就实现了加点的操作。

void add(int d)//加点 
{
	int k=rankth(rt,d);
	ptt x=split(rt,k);
	Treap* p=new Treap();
	p->ch[0]=p->ch[1]=null;p->siz=1;
	p->rnd=rand();p->val=d;p->updata();
	rt=merge(x.first,merge(p,x.second));
	return ;
}

删点

通过分裂与合并,删点也很容易实现。

void del(int d)//删点 
{
	int k=rankth(rt,d);
	ptt x=split(rt,k);
	ptt y=split(x.second,1);
	rt=merge(x.first,y.second);
}

查询排名

其实与Treap的打法也差不多,并没有什么大的差异。

int rankth(Treap* p,int x)//查询排名 
{
	if(p==null) return 0;
	if(p->val>=x) return rankth(p->ch[0],x);
	return rankth(p->ch[1],x)+1+p->ch[0]->siz;
}

查询第k小

查询的话与Treap有一些区别,分裂合并一趟就可以了。

int kth(int k)//查询第k小 
{
	ptt x=split(rt,k-1),y=split(x.second,1);
	Treap* p=y.first;
	rt=merge(x.first,merge(p,y.second));
	if(p==null) return 0;
	return p->val;
}

例题

照样可以交一下普通平衡树那道题,毕竟大部分平衡树都可以交。

#include<cstdio>
#include<cmath>
#include<cstring>
#include<iostream>
#include<algorithm>
#define MAXN 80005
using namespace std;
typedef long long LL;
const int INF=0x7f7f7f7f;
struct Treap
{
	Treap* ch[2];
	int siz,rnd,val;
	void updata(){
		siz=1+ch[0]->siz+ch[1]->siz;
	}
};
Treap* null=new Treap();
Treap* rt=null;
typedef pair<Treap*,Treap*> ptt;
Treap* merge(Treap* a,Treap* b)//合并 
{
	if(a==null) return b;
	if(b==null) return a;
	if(a->rnd<b->rnd){
		a->ch[1]=merge(a->ch[1],b);
		a->updata();
		return a;
	}
	b->ch[0]=merge(a,b->ch[0]);
	b->updata();
	return b;
}
ptt split(Treap* p,int x)//分裂 
{
	if(p==null) 
		return ptt(null,null);
	ptt y;
	if(p->ch[0]->siz>=x){
		y=split(p->ch[0],x);
		p->ch[0]=y.second;
		p->updata();
		y.second=p;
		return y;
	}
	y=split(p->ch[1],x-1-p->ch[0]->siz);
	p->ch[1]=y.first;p->updata();y.first=p;
	return y;
}
int rankth(Treap* p,int x)//查询排名 
{
	if(p==null) return 0;
	if(p->val>=x) return rankth(p->ch[0],x);
	return rankth(p->ch[1],x)+1+p->ch[0]->siz;
}
int kth(int k)//查询第k小 
{
	ptt x=split(rt,k-1),y=split(x.second,1);
	Treap* p=y.first;
	rt=merge(x.first,merge(p,y.second));
	if(p==null) return 0;
	return p->val;
}
void add(int d)//加点 
{
	int k=rankth(rt,d);
	ptt x=split(rt,k);
	Treap* p=new Treap();
	p->ch[0]=p->ch[1]=null;p->siz=1;
	p->rnd=rand();p->val=d;p->updata();
	rt=merge(x.first,merge(p,x.second));
	return ;
}
void del(int d)//删点 
{
	int k=rankth(rt,d);
	ptt x=split(rt,k);
	ptt y=split(x.second,1);
	rt=merge(x.first,y.second);
}
int n;
signed main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		int opt,x;
		scanf("%d %d",&opt,&x);
		if(opt==1) add(x);
		if(opt==2) del(x);
		if(opt==3) printf("%d\n",rankth(rt,x)+1);
		if(opt==4) printf("%d\n",kth(x));
		if(opt==5) printf("%d\n",kth(rankth(rt,x)));
		if(opt==6) printf("%d\n",kth(rankth(rt,x+1)+1));
	}
	return 0;
}

可持久化

一切可支持操作都可以通过以上四个基本操作完成:
  • Build可以用来O(n)构树还可以在替罪羊树套Treap暴力重构的时候降低一个log 的复杂度。
  • MergeSplit可用提取区间,因此可以操作一系列区间操作。
  • Newnode单独拿出来很必要,这样在可持久化的时候会很轻松。

可持久化是对数据结构的一种操作,即保留历史信息,使得在后面可以调用之前 的历史版本。

对于可持久化,我们可以先来看看线段树是怎么可持久化的:
  • 由于只有父亲指向儿子的关系,所以我们可以在线段树进入修改的时候把沿途所 有节点都copy一遍。
  • 然后把需要修改的指向儿子的指针修改一遍就好了,因为每次都是在原途上覆盖, 不会修改前一次的信息。
  • 由于每次只会copy一条路径,而我们知道线段树的树高是log的,所以时空复 杂度都是nlog(n)。
我们来看看旋转的Treap ,现在应该知道为什么不能可持久化了吧?
  • 如果带旋转,那么就会破环原有的父子关系,破环原有的路径和树形态,这是可 持久化无法接受的。
  • 如果把Treap变为非旋转的,那么我们可以发现只要可以可持久化MergeSplit 就可一完成可持久化。

因为上文说到了‘一切可支持操作都可以通过以上四个基本操作完成’,而 Build操作只用于建造无需理会,Newnode就是用来可持久化的工具。

我们来观察一下MergeSplit,我们会发现它们都是由上而下的操作!

因此我们完全可以参考线段树的可持久化对它进行可持久化。

每次需要修改一个节点,就Newnode出来继续做就可以了。


谢谢!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值