平衡树1

简介

Treap 是 Tree 与 Heap 的合并词,是一种比较基础的平衡树。

BST,即二叉搜索树,是一种支持插入、删除、多种查询的数据结构,操作的期望复杂度均为 \(O(\log n)\)。它每个节点有一个键值,它的性质是:任节点左儿子为根的子树中所有节点的键值<该节点键值<右儿子为根的子树中所有节点的键值。通常不构造两个键值相同的节点,因此上面的小于号没有带等;如果必须插入两个相同的键值,建议在原有节点上记录其出现次数,也就是该键值所对节点的出现次数为 2。

容易发现:BST 的中序遍历是单调递增的序列。

BST 的插入操作:从根节点出发,如果要插入的值小于当前节点的值则在左子树中递归插入,如果等于就把出现次数加一并回溯,如果大于就在右子树中递归插入;如果某时刻被牵着鼻子走到的当前节点为空就在这里新建节点,键值设为要插入的值,然后回溯。

查询操作,同理。

删除操作:找到要删除的节点,如果出现次数大于 1 就直接将出现次数减一。否则:如果该节点只有左儿子或右儿子,就令它代替该节点的位置。若不是,找到该节点的后继节点,容易发现,这个节点没有左子节点,因此直接删除它然后让它代替原节点。

所谓前驱、后继是指键值小于这个节点键值的所有节点中键值最大的一个,以及键值大于这个节点键值的所有节点中键值最小的一个。注意最小键值的节点没有前驱,最大键值的节点没有后继。为此避免判断边界,我们一般在 BST 中额外添两个点 -INF 和 INF,初始化 BST 根为-INF,根的右儿子为 INF。

查目标节点的前驱:从根节点出发找目标节点,用途中经过的节点更新前驱,找到后:如果它有左儿子,到它左儿子那里去,然后一直走向右儿子走到尽头,尽头的节点就是前驱;没有左儿子,更新到现在的那个答案就是前驱。

查后继,同理。

我们发现,当插入一系列值而这些值是单调上升或单调下降时,BST 会退化成一条链。怎么办呢?这时平衡树就出现了。平衡树就是一棵“平衡”的 BST。所谓平衡顾名思义,比如,自然一棵满二叉树就比一条链要平衡。如何将一棵树变得平衡呢?我们引入旋转这个概念。

单旋转

单旋转是 Treap 的核心操作之一。单旋转分为右旋 zig 和 左旋 zag。以原来处于父节点位置的节点作为主语,它旋转后相对位置靠右,而左旋后相对位置靠左。以一个节点为主语进行左右旋他旋转后相对位置靠下。

图片.png

叫做单旋转因为只旋转了一次,还有双旋转不过不是 Treap 这节里的。

我们发现要使它还满足 BST 性质,必须要向如图所示那样连接新状态下的节点和子树。我们归纳左旋为三部,右旋也同理:
(1)让 y 成为 p 的左儿子(2)让 p 成为 q 的右儿子(3)让 q 代替 p 的位置,或,让 q 成为原来 p 的父亲的孩子。使用引用可以在代码中省去 p 的父节点的判断。

void zig(int &p){
	int q=a[p].l;
	a[p].l=a[q].r,a[q].r=p;
	p=q;
	Update(a[p].r),Update(p);
}
void zag(int &p){
	int q=a[p].r;
	a[p].r=a[q].l,a[q].l=p;
	p=q;
	Update(a[p].l),Update(p);
}

单单旋转好像没有什么带来什么区别。发明 Treap 的人想到大根堆的性质,然后给每个节点多加了一个键值,我们姑且称这个键值为附加值,每个节点的附加值让它随机产生,因为一棵随机的 BST 可以看作总是平衡的。根据附加值让 Treap 总满足大根堆性质,这也是为什么它名字里有 Heap 的原因。

我们在 Treap 中插入节点并赋一个附加值就需要时时调整结构使满足堆性质。因为可以旋转我们考虑删除操作不那么复杂,可直接把要删除的点旋到叶子,并直接删除。注意反悔的时候要维护堆。

【模板】普通平衡树

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5,INF=1e9;
struct Treap {
	int l,r;
	int val,dat;
	int cnt,size;
}a[N]; int root,tot;
int New(int val){
	a[++tot].val=val;
	a[tot].dat=rand();
	a[tot].cnt=a[tot].size=1;
	return tot;
}
void Update(int p){
	a[p].size=a[a[p].l].size+a[a[p].r].size+a[p].cnt;
}
void Build(){
	New(-INF),New(INF);
	root=1,a[1].r=2;
	Update(root);
}
void zig(int &p){
	int q=a[p].l;
	a[p].l=a[q].r,a[q].r=p;
	p=q;
	Update(a[p].r),Update(p);
}
void zag(int &p){
	int q=a[p].r;
	a[p].r=a[q].l,a[q].l=p;
	p=q;
	Update(a[p].l),Update(p);
}
void Insert(int &p,int val){
	if(p==0){
		p=New(val);
		return;
	}
	if(val==a[p].val){
	    a[p].cnt++,Update(p);
	    return;
	}
	if(val<a[p].val){
	    Insert(a[p].l,val);
	    if(a[p].dat<a[a[p].l].dat) zig(p);
	}
	else {
	    Insert(a[p].r,val);
	    if(a[p].dat<a[a[p].r].dat) zag(p);
	}
	Update(p);
}
void Remove(int &p,int val){
	if(p==0) return;
	if(val==a[p].val){
		if(a[p].cnt>1){
			a[p].cnt--,Update(p);
			return;
		}
		if(a[p].l || a[p].r){
			if(a[p].r==0 || a[a[p].l].dat>a[a[p].r].dat)
				zig(p),Remove(a[p].r,val);
			else zag(p),Remove(a[p].l,val);
			Update(p);
		}
		else p=0;
		return;
	}
	val<a[p].val?Remove(a[p].l,val):Remove(a[p].r,val);
	Update(p);
}
int GetPre(int val){
	int ans=1;
	int p=root;
	while(p){
		if(val==a[p].val){
			if(a[p].l>0){
				p=a[p].l;
				while(a[p].r>0) p=a[p].r;
				ans=p;
			}
			break;
		}
		if(a[p].val<val && a[p].val>a[ans].val) ans=p;
		p=val<a[p].val?a[p].l:a[p].r;
	}
	return a[ans].val;
}
int GetNext(int val){
	int ans=2;
	int p=root;
	while(p){
		if(val==a[p].val){
			if(a[p].r>0){
				p=a[p].r;
				while(a[p].l>0) p=a[p].l;
				ans=p;
			}
			break;
		}
		if(a[p].val>val && a[p].val<a[ans].val) ans=p;
		p=val<a[p].val?a[p].l:a[p].r;
	}
	return a[ans].val;
}
int GetRankByVal(int p,int val){
	if(p==0) return 0;
	if(val==a[p].val) return a[a[p].l].size+1;
	if(val<a[p].val) return GetRankByVal(a[p].l,val);
	return GetRankByVal(a[p].r,val)+a[a[p].l].size+a[p].cnt;
}
int GetValByRank(int p,int rank){
	if(p==0) return INF;
	if(rank<=a[a[p].l].size) return GetValByRank(a[p].l,rank);
	if(rank<=a[a[p].l].size+a[p].cnt) return a[p].val;
	return GetValByRank(a[p].r,rank-a[a[p].l].size-a[p].cnt);
}
int main()
{
	int m;
	cin>>m;
	Build();
	int opt,x;
	while(m--){
		cin>>opt>>x;
		switch(opt){
			case 1:
				Insert(root,x);
				break;
			case 2:
				Remove(root,x);
				break;
			case 3:
				cout<<GetRankByVal(root,x)-1<<'\n'; //-1是因为-INF占了一个rank 
				break;
			case 4:
				cout<<GetValByRank(root,x+1)<<'\n'; //+1是因为-INF是"rank 1" 
				break;
			case 5:
				cout<<GetPre(x)<<'\n';
				break;
			case 6:
				cout<<GetNext(x)<<'\n';
				break;
		}
	}
	return 0;
}

练习

posted @ 2021-06-30 18:45  pengyule  阅读(43)  评论(0编辑  收藏  举报