FHQ Treap学习笔记

前言

FHQ Treap又名「无旋Treap」与普通的Treap一样,每个节点都维护两个信息,一个是权值,一个是优先级,在维持节点权值满足BST的同时使优先级满足二叉堆性质,在所有平衡树中,FHQ Treap应该是码量很小的一种平衡树了。

变量名声明

我们在这里约定:

struct data{
    int l,r,sz,prio,key;
    //l为左儿子
    //r为右儿子
    //sz为子树大小
    //prio为随机生成的优先级
    //key为权值 
}t[N];

主要操作

FHQ Treap的操作主要是\(split\)\(merge\),其他的操作也都在这两个操作的前提下执行。

split

\(split\)又分为两种:
1.按值分裂,即以\(Key\)为标准,将小于等于\(Key\)的权值分到一棵树中,将大于\(Key\)的权值分到另一棵树中。
2.按子树大小分裂,即以\(size\)为标准,一棵树的大小为\(size\),另一棵树的大小为总节点数减\(size\)
我们以按值分裂为例。假设我们分出来的两棵树分别为\(x,y\),且\(\forall i\in x,j\in y,key_i<key_j\)
假设我们当前走到的节点为\(p\),这里分两种情况:
1.\(key_p \leq Key\)
因为当前节点的权值小于等于\(Key\),所以它应该被划分到\(x\)这棵子树中,而因为这是一棵\(BST\),所以\(p\)的左儿子的值一定小于等于\(p\)的值,所以我们只需要递归下去分裂\(p\)的右儿子即可。
2.\(key_p > Key\)
\(key_p \leq Key\),只需要反过来即可。

代码实现

void split(int p,int key,int &x,int &y){//p表示当前节点,key表示按值分裂的权值,x表示x这棵子树的缺口,y同理 
	if(p==0) {
		x=y=0;
		return;
	}
	if(t[p].key<=key){
		x=p;//把x的缺口补上 
		split(t[p].r,key,t[p].r,y);
		//第一个t[p].r表示递归右儿子
		//分裂的值不变 
		//第二个t[p].r:因为p的左儿子的值都小于等于p的值所以一并分裂出去了,现在p的右儿子是空的,所以我们要补上它的缺口
		//因为y不变,所以还是y 
	}else{
		y=p;//同上,反过来就行了 
		split(t[p].l,key,x,t[p].l);//同上 
	}
	push(p);//每次操作都要重新维护一下sz 
}

按子树大小分裂也差不多,就不讲了,这里给一下代码:

void split(int p,int sz,int &x,int &y){
	if(p==0) {
		x=y=0;
		return;
	}
	if(t[t[p].l].sz+1<=sz){//还要加上自己
		x=p;
		split(t[p].r,sz-t[t[p].l].sz-1,t[p].r,y);
	}else{
		y=p;
		split(t[p].l,sz,x,t[p].l);
	}
	push(p);//更新sz
}

merge

假设我们当前要合并的两棵子树为\(x,y\),且\(\forall i\in x,j\in y,key_i<key_j\)
假设\(prio_x>prio_y\)因为要符合二叉堆的性质,这里以大根堆为例,那么优先级大的就应该在上面,又因为要符合\(BST\)的性质,所以\(y\)\(x\)的右儿子,但是原本\(x\)就已经有右儿子了,所以我们要把\(r_x\)\(y\)合并的结果变成\(x\)的右儿子,如此递归下去。\(prio_x<prio_y\)同理。

代码实现

int merge(int x,int y){//合并x和y 
	if(!x||!y)//如果有一个为空,那么显然剩下的那个就是合并结果 
		return x+y;//返回不为空的那个 
	if(t[x].prio>t[y].prio){//x的优先级更大 
		t[x].r=merge(t[x].r,y);//合并x原来的右儿子和y 
		push(x);//更新sz 
		return x;//合并后的结果是x 
	}else{
		t[y].l=merge(x,t[y].l);//同上 
		push(y);
		return y;
	}
}

其他操作

更新

只需要维护一下\(size\)即可。

void push(int p){
	if(p)
		t[p].sz=t[t[p].l].sz+t[t[p].r].sz+1;
}

建立新节点

int creat(int key){//节点的权值为key
	int p=++tot;
	t[p].l=t[p].r=0;
	t[p].sz=1;
	t[p].prio=rand();
	t[p].key=key;
	return p;
}

插入权值为key的节点

首先把整棵树按照权值\(key-1\)分裂成两棵树\(x,y\),然后将插入的值建一个节点,合并\(x\)和新节点,再把\(y\)和第一次合并的结果合并。

void insert(int key){
	int x,y;
	split(root,key-1,x,y);
	root=merge(merge(x,creat(key)),y);
}

删除权值为key的节点

先把整棵树按照权值\(key\)分裂成两棵树\(x,z\),再把小于等于\(key\)的那棵树按照权值\(key-1\)分裂成两棵树\(x,y\),然后直接合并\(x,z\)即可。

void del(int key){
	int x,y,z;
	split(root,key,x,z);
	split(x,key-1,x,y);
	if(y){
		y=merge(t[y].l,t[y].r);//只删一个节点
	}
	root=merge(merge(x,y),z);
}

权值为key的前驱

按照权值\(key-1\)分裂成两棵树\(x,y\),再在\(x\)当中不断走右儿子即可。

int pre(int key){
	int x,y,now;
	split(root,key-1,x,y);
	now=x;
	while(t[now].r) now=t[now].r;
	int res=t[now].key;
	root=merge(x,y);
	return res;
}

权值为key的后继

按照权值\(key\)分裂成两棵树\(x,y\),再在\(y\)当中不断走左儿子即可。

int las(int key){
	int x,y,now;
	split(root,key,x,y);
	now=y;
	while(t[now].l) now=t[now].l;
	int res=t[now].key;
	root=merge(x,y);
	return res;
}

权值为key的排名

按照权值\(key-1\)分裂成两棵树\(x,y\),答案就是子树\(x\)的大小再加\(1\)

int rk(int key){
	int x,y;
	split(root,key-1,x,y);
	int ans=t[x].sz+1;
	root=merge(x,y);
	return ans;
}

排名为k的值

不断在左右儿子中走,假设当前节点是\(p\),如果\(l_{p_{sz}}+1=k\),那么说明当前节点即为答案。
如果\(l_{p_{sz}}+1<k\),那么说明答案在\(p\)的右儿子中,\(k\)减去\(l_{p_{sz}}+1\)之后,令\(p=r_p\)
如果\(l_{p_{sz}}+1>k\),那么说明答案在\(p\)的左儿子中,令\(p=l_p\)

int rk_key(int rank){
	int p=root;
	while(1){
		if(t[t[p].l].sz+1==rank)
			break;
		else if(t[t[p].l].sz+1>rank)
			p=t[p].l;
		else if(t[t[p].l].sz+1<rank)
			rank-=t[t[p].l].sz+1,p=t[p].r;
	}
	return t[p].key;
}

一些简单题

【模板】普通平衡树
上面所有代码再加一个主函数就可以\(A\)了。
[NOI2004] 郁闷的出纳员
模板题,注意一点技巧,用一个变量计算加减。
【模板】文艺平衡树
打一个翻转标记即可。
序列终结者
也是打标记。

posted @ 2021-02-25 20:34  谁伴我流浪  阅读(104)  评论(0编辑  收藏  举报