[OI] 平衡树

1. 二叉查找树

二叉查找树的思想和优先队列比较像,都是把若干个数据按一定规则插到一棵树里,然后就可以维护特定的信息.

在优先队列的大根堆实现里,我们让每棵子树的根节点都大于它的儿子,这样就可以保证根节点一定是那个最大值,也就是我们需要的最值操作.

那么二叉查找树,顾名思义是可以查找特定 \(rank\) 或类似 \(lower\_bound()\) 的元素的树,通常为了实现这个我们是这样定义的:

  1. 令左子节点小于父节点
  2. 令右子节点大于父节点

比较容易想到,这样我们维护一个 \(size\) 就能快速二分查找答案了.

但是二叉查找树对链式结构非常敏感,因此需要进行各种优化. 二叉查找树有不少优化统称为平衡树,但核心思想都是一样的.

那么是什么核心思想呢,首先我们可以注意到,假如我们对二叉查找树中序遍历,那么得到的序列一定是有序的,因此我们需要保证在优化结构的同时,还要保证中序遍历不变,否则就无法再查找答案了. 这就是平衡树的思想.

下面对每种平衡树进行具体展开.

2.Splay

发明 : \(\texttt{Daniel Sleator}\) and \(\texttt{Robert Tarjan}\) (1985)

P6136 [C++14 -O2] 平均耗时 \(24.9450\) s | [C++14] 平均耗时 \(24.6067\) s

2.1 算法思想

基本思路:将一个点的父节点移到它的子节点的子节点上

特点:跑得比较快,不好写,代码不好理解,总体来看不如 Treap,所以不太详细讲

下面我们设 \(r\ (root)\)\(x\) 的父节点,并且 \(x\) 为左孩子. 可以发现,根据二叉平衡树的性质,一定会有 \(w_{r}\ge w_{x}\),因此我们应该将 \(r\) 放在 \(x\) 的右孩子位置.

那么假如 \(x\) 已经有了右孩子 \(s\) 怎么办呢,还是根据二叉平衡树的性质,应该有 \(w_{s}\le w_{r}\),因此我们把 \(s\) 直接放到 \(y\) 的左孩子上,因为 \(r\) 的左孩子是 \(x\) ,因此 \(r\) 现在一定没有左孩子了,正好可以放进去. 这样的操作我们称为左旋.

右旋类似,只不过 \(x\) 变成了 \(r\) 的右孩子.

现在我们又有了两个新的问题:如何利用这些操作优化结构?如何实现这种优化?

Splay 对于每个修改/查询的节点,都首先把它翻到根节点的位置,这样来保证频繁查找的节点距离根节点最近. 可以证明,Splay 的修改/查询的复杂度均摊 \(log\ n\).

但是实际上我们会发现,在把 \(x\) 旋转到根节点的过程中,树的结构完全没有得到优化,这是因为我们仅仅旋转了 \(x\),假设 \(x\) 的祖先是 \(y\)\(y\) 的祖先是 \(z\),假如现在 \(x\)\(y\) 的左子树,\(y\)\(x\) 的左子树,那么无论如何旋转 \(x\),树的深度是永远也不会变的,所以我们需要特殊处理这种情况,来让树的深度减小一点.

注意到我们可以先旋转 \(y\),强行把 \(x\)\(z\) 拉到同一个深度,这样就可以继续旋转 \(x\) 了,这就是 Splay 的基本思想.

2.2 核心代码

2.2.1 更新

目标:统计更新节点 \(x\) 的子树大小

inline void update(int x){
	if(x){
		t[x].size=t[x].cnt;
		if(t[x].son[0]) t[x].size+=t[t[x].son[0]].size;
		if(t[x].son[1]) t[x].size+=t[t[x].son[1]].size;
	}
}

2.2.2 旋转

目标:对 \(x\) 执行一次旋转操作,使其与其父节点 \(y\) 交换位置.

基本步骤:

  1. 确认要旋转的点 \(x\) 是左孩子还是右孩子
  2. 按上述规律旋转 \(x\)(将 \(y\) 变为 \(x\) 异侧儿子,并将 \(x\) 被替换的孩子换到 \(y\) 的同侧)
inline void rotate(int x){
	int f=t[x].fa,gf=t[f].fa;
	int k=judgeson(x);
    //judgeson():左孩子返回0,否则返回1
	t[f].son[k]=t[x].son[k^1];
    //k^1 也可用 1-k 代替
	t[t[x].son[k^1]].fa=f;
	t[x].son[k^1]=f;
	t[f].fa=x;
	t[x].fa=gf;
	if(gf){
		t[gf].son[t[gf].son[1]==f]=x;
	}
	update(f);update(x);
    //记得 update
} 

2.2.3 Splay

目标:将 \(x\) 旋转到根节点.

基本步骤:

  1. 反复对 \(x\) 执行旋转操作,直到 \(x\) 为根节点.
  2. 特殊地,若 \(x,y,z\ (y=father_{x},z=father_{y})\) 满足上述条件(\(x\)\(y\) 的左子树,\(y\)\(x\) 的左子树,或同为右子树),那么先旋转 \(y\),再旋转 \(x\).
inline void splay(int x){
	for(int f;f=t[x].fa;rotate(x)){
        //f=t[x].fa 这句实际上是 f=t[x].fa 和 f!=0 合起来
		if(t[f].fa){
            //这里没必要再else了,t[f].fa=0的情况一定会在下一次被跳出去
			if(judgeson(x)==judgeson(f)){
                //特殊情况
				rotate(f);
			}
			else rotate(x);
		}
	}
	root=x;
}

2.2.4 插入

目标:插入一个值为 \(x\) 的元素

基本步骤:

  1. 若树为空,新建节点并设置树根.
  2. 否则从树根开始查找元素 \(x\),若找到则该元素数量加一,否则新建一个节点.
inline void insert(int x){
	if(!root){
        //树为空
		tot++;
		t[tot]={x,1,1,0,{0,0}};
		root=tot;
		return;
	}
	int now=root,fa=0;
	while(1){
		if(x==t[now].w){
            //找到元素x
			t[now].cnt++;
			update(now);update(fa);
			splay(now);
			break;
		}
		fa=now;
		now=t[now].son[t[now].w<x];
        //利用二叉平衡数的特殊性质跳转
		if(!now){
            //始终未找到x,新建节点
			tot++;
			t[tot]={x,1,1,fa,{0,0}};
			t[fa].son[t[fa].w<x]=tot;
			update(fa);
			splay(tot);
			break;
		}
	}
}

2.2.5 查询元素

目标:查找 \(rank\) 值为 \(x\) 的数

基本步骤:

  1. 根据二叉平衡树的性质,将 \(x\) 不断减去当前节点的左子树大小(左子树节点的 \(rank\) 一定更小)来逼近答案
  2. 直到逼近成负数,返回当前值
  3. 特殊地,若到达根节点,需要手动跳出,防止死循环
inline int findnum(int x){
	int now=root;
	while(now){
		if(t[now].son[0] and x<=t[t[now]son[0]].size){
            //这里必须要判,不然会多减
			now=t[now].son[0];
		}
		else{
            //减不动了再判断
			int temp=t[now].cnt;
			if(t[now].son[0]){
				temp+=t[t[now].son[0]].size;
			}
			if(x<=temp) return t[now].w;
			x-=temp;
			now=t[now].son[1];
		}
	}
	return t[now].w;
    //特殊处理
}

2.2.6 查询 \(Rank\)

目标:查找值为 \(x\) 的元素的 \(rank\)

基本步骤:

  1. 先跳到值 \(x\) 的位置上,路上不断统计左子树大小(即 \(rank\) 比当前数小的数的数量)
  2. 特殊地,若跳到根节点,需要手动退出
  3. 更特殊地,本函数统计的是小于元素 \(x\) 的元素个数,请依题目描述适当更改
inline int findrank(int x){
	int now=root,ans=0;
	while(now){
		if(x<t[now].w){
			now=t[now].son[0];
            //先尽可能往小跳
		}
		else{
            //跳不动了开始处理
			if(t[now].son[0]){
				ans+=t[t[now].son[0]].size;
                //累加答案
			}
			if(x==t[now].w){
				splay(now);
				return ans;
			}
			ans+=t[now].cnt;
			now=t[now].son[1]; 
		}
	}
	return ans;
    //特殊处理
}

2.2.7 查询前驱

目标:查找第一个比元素 \(x\) 小的元素(即左子树的最右子树)

inline int findpre(){
	int now=t[root].son[0];
	while(t[now].son[1]) now=t[now].son[1];
	return now;
}

2.2.8 查询后缀

目标:查找第一个比元素 \(x\) 大的元素(即右子树的最左子树)

inline int findnext(){
	int now=t[root].son[1];
	while(t[now].son[0]) now=t[now].son[0];
	return now;
}

2.2.9 删除节点

目标:删除值为 \(x\) 的元素中的其中一个

基本步骤:

  1. 先把 \(x\) 跳到根节点
  2. 假如不止一个元素,那减掉一个即可
  3. 否则应该删掉这个点,删除后应该把当前节点的儿子与父亲全部转移到它的前驱节点上.
  4. 特殊地,对于该节点只有左儿子,只有右儿子或者都有,都没有的四种情况应分别讨论
inline void free(int x){
	findrank(x);
	if(t[root].cnt>1){
        //不止一个
		t[root].cnt--;
		update(root);
		return;
	}
	if(!t[root].son[0] and !t[root].son[1]){
        //一个儿子也没有
		clear(root);
		root=0;
		return;
	}
	if(!t[root].son[0]){
        //只有右儿子
		int oroot=root;
		root=t[root].son[1];
		t[root].fa=0;
		clear(oroot);
		return;
	}
	else if(!t[root].son[1]){
        //只有左儿子
		int oroot=root;
		root=t[root].son[0];
		t[root].fa=0;
		clear(oroot);
		return;
	}
    //全有
	int left=findpre(),oroot=root;
	splay(left);
	t[root].son[1]=t[oroot].son[1];
	t[t[oroot].son[1]].fa=root;
	clear(oroot);
	update(root);
}

2.3 完整代码

2.3.1 版本一 (功能不全)

namespace splay{
	const int N=1000001;
	#define tol(id) t[t[id].son[0]]
	#define tor(id) t[t[id].son[1]]
	int root,tot;
	struct tree{
		int w;
		int tot,size;
		int fa,son[2];
	}t[N];
	inline void update(int id){
		t[id].size=tol(id).size+tor(id).size+t[id].tot;
	}
	inline void rotate(int x){
		int y=t[x].fa,z=t[y].fa;
		bool rs;
		if(t[y].son[0]==x) rs=0;
		else rs=1;
		t[z].son[t[z].son[1]==y]=x;
		t[x].fa=z;
		t[y].son[rs]=t[x].son[rs^1];
		t[t[x].son[rs^1]].fa=y;
		t[x].son[rs^1]=y;
		t[y].fa=x;
		update(y);update(x);
	}
	inline void splay(int x,int k){
		while(t[x].fa!=k){
			int y=t[x].fa,z=t[y].fa;
			if(z!=k){
				if((t[z].son[0]==y)^(t[y].son[0]==x)){
					rotate(x);
				}
				else rotate(y);
			}
			rotate(x);
		}
		if(k==0) root=x;
	}
	inline void find(int x){
		int u=root;
		if(!u) return;
		while(t[u].son[x>t[u].w] and x!=t[u].w){
			u=t[u].son[x>t[u].w];
		}
		splay(u,0);
	}
	inline void insert(int x){
		int u=root,f=0;
		while(u and t[u].w!=x){
			f=u;
			u=t[u].son[x>t[u].w];
		}
		if(u) t[u].tot++;
		else{
			u=++tot;
			if(f) t[f].son[x>t[f].w]=u;
			t[u].son[0]=t[u].son[1]=0;
			t[tot]={x,1,1,f,{0,0}};
		}
		splay(u,0);
	}
	inline int findnext(int x,bool isnext){
		find(x);
		int u=root;
		if(t[u].w>x and isnext) return u;
		if(t[u].w<x and !isnext) return u;
		u=t[u].son[isnext];
		while(t[u].son[isnext^1]) u=t[u].son[isnext^1];
		return u;
	}
	inline void free(int x){
		int last=findnext(x,0);
		int next=findnext(x,1);
		if(last) splay(last,0);
		splay(next,last);
		int del=t[next].son[0];
		if(t[del].tot>1){
			t[del].tot--;
			splay(del,0);
		}
		else t[next].son[0]=0;
	}
	inline int find(int x){
		int u=root,ans=0;
		while(u){
			if(x<t[u].w) u=t[u].son[0];
			else{
				if(t[u].son[0]) ans+=t[t[u].son[0]].size;
				if(x==t[u].w){
					splay(u,0);
					return ans+1;
				}
				ans+=t[u].tot;
				u=t[u].son[1];
			}
		}
		return ans+1;
	}
}

2.3.2 版本二

#include<bits/stdc++.h>
using namespace std;
namespace splay{
	const int N=1000001;
	#define tol(i) t[(i)].son[0]
	#define tor(i) t[(i)].son[1]
	#define to(i,j) t[(i)].son[(j)]
	struct splaytree{
		int root,tot;
		struct tree{
			int w;
			int cnt,size;
			int fa,son[2];
		}t[N];
		inline void clear(int x){
			t[x]={0,0,0,0,{0,0}};
		}
		inline bool judgeson(int x){
			return t[t[x].fa].son[1]==x;
		}
		inline void update(int x){
			if(x){
				t[x].size=t[x].cnt;
				if(t[x].son[0]) t[x].size+=t[t[x].son[0]].size;
				if(t[x].son[1]) t[x].size+=t[t[x].son[1]].size;
			}
		}
		inline void rotate(int x){
			int f=t[x].fa,gf=t[f].fa;
			int k=judgeson(x);
			t[f].son[k]=t[x].son[k^1];
			t[t[f].son[k]].fa=f;
			t[x].son[k^1]=f;
			t[f].fa=x;
			t[x].fa=gf;
			if(gf){
				t[gf].son[t[gf].son[1]==f]=x;
			}
			update(f);update(x);
		} 
		inline void splay(int x){
			for(int f;f=t[x].fa;rotate(x)){
				if(t[f].fa){
					if(judgeson(x)==judgeson(f)){
						rotate(f);
					}
					else rotate(x);
				}
			}
			root=x;
		}
		inline void insert(int x){
			if(!root){
				tot++;
				t[tot]={x,1,1,0,{0,0}};
				root=tot;
				return;
			}
			int now=root,fa=0;
			while(1){
				if(x==t[now].w){
					t[now].cnt++;
					update(now);update(fa);
					splay(now);
					break;
				}
				fa=now;
				now=t[now].son[t[now].w<x];
				if(!now){
					tot++;
					t[tot]={x,1,1,fa,{0,0}};
					t[fa].son[t[fa].w<x]=tot;
					update(fa);
					splay(tot);
					break;
				}
			}
		}
		inline int findnum(int rank){
			int now=root;
			while(now){
				if(t[now].son[0] and rank<=t[t[now].son[0]].size){
					now=t[now].son[0];
				}
				else{
					int temp=t[now].cnt;
					if(t[now].son[0]){
						temp+=t[t[now].son[0]].size;
					}
					if(rank<=temp) return t[now].w;
					rank-=temp;
					now=t[now].son[1];
				}
			}
			return t[now].w;
		}
		inline int findrank(int val){
			int now=root,ans=0;
			while(now){
				if(val<t[now].w){
					now=t[now].son[0];
				}
				else{
					if(t[now].son[0]){
						ans+=t[t[now].son[0]].size;
					}
					if(val==t[now].w){
						splay(now);
						return ans;
					}
					ans+=t[now].cnt;
					now=t[now].son[1]; 
				}
			}
			return ans;
		}
		inline int findpre(){
			int now=t[root].son[0];
			while(t[now].son[1]) now=t[now].son[1];
			return now;
		}
		inline int findnext(){
			int now=t[root].son[1];
			while(t[now].son[0]) now=t[now].son[0];
			return now;
		}
		inline int findpre(int val){
			insert(val);
			int ans=findpre();
			free(val);
			return ans;
		}
		inline int findnext(int val){
			insert(val);
			int ans=findnext();
			free(val);
			return ans;
		}
		inline int got(int id){
			return t[id].w;
		}
		inline void free(int x){
			findrank(x);
			if(t[root].cnt>1){
				t[root].cnt--;
				update(root);
				return;
			}
			if(!t[root].son[0] and !t[root].son[1]){
				clear(root);
				root=0;
				return;
			}
			if(!t[root].son[0]){
				int oroot=root;
				root=t[root].son[1];
				t[root].fa=0;
				clear(oroot);
				return;
			}
			else if(!t[root].son[1]){
				int oroot=root;
				root=t[root].son[0];
				t[root].fa=0;
				clear(oroot);
				return;
			}
			int left=findpre(),oroot=root;
			splay(left);
			t[root].son[1]=t[oroot].son[1];
			t[t[oroot].son[1]].fa=root;
			clear(oroot);
			update(root);
		}
	};
}
using namespace splay;

2.3.3 版本三:完善版

#include<bits/stdc++.h>
using namespace std;
namespace splay{
	const int N=1000001;
	int w[N];
	const int inf=114514191;
	struct splaytree{
		int root,tot;
		vector<int> search_answer;
		struct tree{
			int w;
			int cnt,size;
			int fa,son[2];
			int tag;
		}t[N];
		inline void clear(int x){
			t[x]={0,0,0,0,{0,0}};
		}
		inline bool judgeson(int x){
			return t[t[x].fa].son[1]==x;
		}
		inline void update(int x){
			if(x){
				t[x].size=t[x].cnt;
				if(t[x].son[0]) t[x].size+=t[t[x].son[0]].size;
				if(t[x].son[1]) t[x].size+=t[t[x].son[1]].size;
			}
		}
		inline int build(int l,int r,int last){
			if(l>r) return 0;
			int mid=(l+r)/2;
			int now=++tot;
			t[now]={w[mid],1,1,last,{0,0},0};
			t[now].son[0]=build(l,mid-1,now);
			t[now].son[1]=build(mid+1,r,now);
			update(now);
			return now;
		}
		inline void pushdown(int x){
			if(x and t[x].tag){
				t[t[x].son[1]].tag^=1;
				t[t[x].son[0]].tag^=1;
				swap(t[x].son[1],t[x].son[0]);
				t[x].tag=0;
			}
		}
		inline void rotate(int x){
			int f=t[x].fa,gf=t[f].fa;
			pushdown(x),pushdown(f);
			int k=judgeson(x);
			t[f].son[k]=t[x].son[k^1];
			t[t[f].son[k]].fa=f;
			t[x].son[k^1]=f;
			t[f].fa=x;
			t[x].fa=gf;
			if(gf){
				t[gf].son[t[gf].son[1]==f]=x;
			}
			update(f);
		}
		inline void splay(int x){
			for(int f;(f=t[x].fa);rotate(x)){
				if(t[f].fa){
					if(judgeson(x)==judgeson(f)){
						rotate(f);
					}
					else rotate(x);
				}
			}
			root=x;
		}
		inline void splay(int x,int goal){
			for(int f;(f=t[x].fa)!=goal;rotate(x)){
				if(t[f].fa!=goal){
					if(judgeson(x)==judgeson(f)){
						rotate(f);
					}
					else{
						rotate(x);
					}
				}
			}
			if(goal==0){
				root=x;
			}
		}
		inline void insert(int x){
			if(!root){
				tot++;
				t[tot]={x,1,1,0,{0,0}};
				root=tot;
				return;
			}
			int now=root,fa=0;
			while(1){
				if(x==t[now].w){
					t[now].cnt++;
					update(now);update(fa);
					splay(now);
					break;
				}
				fa=now;
				now=t[now].son[t[now].w<x];
				if(!now){
					tot++;
					t[tot]={x,1,1,fa,{0,0}};
					t[fa].son[t[fa].w<x]=tot;
					update(fa);
					splay(tot);
					break;
				}
			}
		}
		inline int find(int x){
			int now=root;
			while(now){
				pushdown(now);
				if(x<=t[t[now].son[0]].size){
					now=t[now].son[0];
				}
				else{
					x-=t[t[now].son[0]].size+1;
					if(!x) return now;
					now=t[now].son[1];
				}
			}
			return 0;
		}
		inline void reverse(int L,int R){
			int l=L-1,r=R+1;
			l=find(l),r=find(r);
			splay(l,0);
			splay(r,l);
			int p=t[root].son[1];
			p=t[p].son[0];
			t[p].tag^=1;
		}
		inline int findnum(int rank){
			int now=root;
			while(now){
				if(t[now].son[0] and rank<=t[t[now].son[0]].size){
					now=t[now].son[0];
				}
				else{
					int temp=t[now].cnt;
					if(t[now].son[0]){
						temp+=t[t[now].son[0]].size;
					}
					if(rank<=temp) return t[now].w;
					rank-=temp;
					now=t[now].son[1];
				}
			}
			return t[now].w;
		}
		inline int findrank(int val){
			int now=root,ans=0;
			while(now){
				if(val<t[now].w){
					now=t[now].son[0];
				}
				else{
					if(t[now].son[0]){
						ans+=t[t[now].son[0]].size;
					}
					if(val==t[now].w){
						splay(now);
						return ans;
					}
					ans+=t[now].cnt;
					now=t[now].son[1]; 
				}
			}
			return ans;
		}
		inline int findpre(){
			int now=t[root].son[0];
			while(t[now].son[1]) now=t[now].son[1];
			return now;
		}
		inline int findnext(){
			int now=t[root].son[1];
			while(t[now].son[0]) now=t[now].son[0];
			return now;
		}
		inline int findpre(int val){
			insert(val);
			int ans=findpre();
			free(val);
			return ans;
		}
		inline int findnext(int val){
			insert(val);
			int ans=findnext();
			free(val);
			return ans;
		}
		inline int got(int id){
			return t[id].w;
		}
		inline void free(int x){
			findrank(x);
			if(t[root].cnt>1){
				t[root].cnt--;
				update(root);
				return;
			}
			if(!t[root].son[0] and !t[root].son[1]){
				clear(root);
				root=0;
				return;
			}
			if(!t[root].son[0]){
				int oroot=root;
				root=t[root].son[1];
				t[root].fa=0;
				clear(oroot);
				return;
			}
			else if(!t[root].son[1]){
				int oroot=root;
				root=t[root].son[0];
				t[root].fa=0;
				clear(oroot);
				return;
			}
			int left=findpre(),oroot=root;
			splay(left);
			t[root].son[1]=t[oroot].son[1];
			t[t[oroot].son[1]].fa=root;
			clear(oroot);
			update(root);
		}
		inline void search(int now){
			pushdown(now);
			if(t[now].son[0]) search(t[now].son[0]);
			if(t[now].w!=inf and t[now].w!=-inf){
				search_answer.push_back(t[now].w);
			}
			if(t[now].son[1]) search(t[now].son[1]);
		}
		inline void printanswer(char devide,char ending){
			for(int i:search_answer){
				cout<<i<<devide;
			}
			cout<<ending;
		}
	};
}
using namespace splay;

2.4 记忆提示

全局变量(2)
结构体(1,1,1,1,2)

update(int): 更新节点 $x$ 的子树大小
hint: 自己+左+右

judgeson(int)
hint: 左0 右1

rotate(int): 旋转当前点与父节点
hint: 
f,gf
(下放)
judgeson(x)
x异侧孩子上移(到x同侧)改fa
fa[x]下移(到异侧)改fa
x改fa到fa[fa[x]],非空改son(fa[x]同侧取代)
更新fa[x]


2.5 常见问题

Splay TLE 了

假如你在任何一个查询操作里死循环而不输出答案,请考虑以下原因:

  1. 注意特判根节点,假如你的代码可能会因为 \(fa_{root}=root\) 死循环.
  2. 请检查有没有进行节点跳转

否则,如果你只是运行速度较慢,请检查你的 Splay 函数,未成功 Splay(或未成功旋转导致效率太低)可能是一个原因.

奇怪的 WA

因为平衡树的各种概念定义并没有一个统一的规范,不同的题目对于各种操作的定义可能是不同的,函数并不能完全做到符合题目的要求,需要进行适当改装.

比如,对于查询 \(rank\) 的操作,假如定义是 “该元素第一个出现的排名”,则需要你将函数的答案加 \(1\),假如定义是 “该元素第最后一个出现的排名”,则需要你将函数的答案加该节点的 \(size\).

3.Treap

P6136 [C++14 -O2] 平均耗时 \(11.8950\) s | [C++14] 平均耗时 \(12.9575\) s

3.1 算法思想

基本思路:Treap=Tree+Heap

特点:跑得非常快,好写,好平衡树,赞了

Tree 在这里指的就是二叉查找树,因为我们要维护二叉查找树的平衡,显然这里必须要有一个. 这也就是说,我们在 Splay 中使用的 rotate() 函数是通用的.

Heap 即为堆,因为我们重点要解决的是退化成链的问题,而二叉堆(优先队列)恰好能够自己平衡自己,所以我们尝试用二叉查找树的性质来维护二叉堆的平衡.

所以我们应该给每个节点都赋两个值,一个是该节点的真实值(用于二叉查找树),另一个是我们为了保持平衡赋的值(用于二叉堆),那么我们需要干的就是利用 rotate() 操作来实现这个二叉堆的操作.

那么我们手动赋的这个值应该是多少呢,不知道,但是可以随便给,理论证明随机给数是大概率平衡的. 我们的随机种子应该是固定的,即使可以换,但是也不应该用 srand(time(0)) 之类的函数(其实没啥影响,但就是会有点影响)

可以看出这个算法还是比较看脸吃饭的

其实 Treap 的实现还是非常简单的. 二叉堆的实现我们都知道:插入的时候先插在最外面,再通过比较进行旋转(只不过这里我们需要维护二叉查找树所以只能左右旋罢了),删除的时候就先换到最下面(怎么换都行),然后删了它再平衡就行了.

3.2 核心代码

3.2.1 更新

更新就和 Splay 的没啥区别了,在这里提供一种更简单的写法,因为子树不存在的话 \(size\) 本来就是 \(0\),加上也没啥影响,因此就不用判了.

inline void update(int x){
	t[x].size=t[t[x].son[0]].size+t[t[x].son[1]].size+t[x].cnt;
}

3.2.2 旋转

同 2.2.2

这里和 Splay 最大的差异就是这里传了一个 &id,其实这里在实现的时候主要是传 \(root\) 进去,所以引用的话能比较方便地改根节点.

这里的 \(isright\) 即可以理解成右旋的 \(right\),也可以理解成左孩子与右孩子(\(isright=1\) 就是把右孩子翻上来)

inline void rotate(int &id,int isrignt){
	bool k=isrignt;
	int temp=t[id].son[k^1];
	t[id].son[k^1]=t[temp].son[k];
	t[temp].son[k]=id;
	id=temp;
	update(t[id].son[k]);
	update(id);
}

3.2.3 新建节点

其实这个函数可以没有的... 按需写吧

inline int newnode(int val){
	t[++tot]={val,rand(),1,1,{0,0}};
	return tot;
}

3.2.4 插入

这里为了方便,我们把函数写成递归的形式

  1. 未找到就新建节点
  2. 如果恰好找到的话,记一个 \(cnt\).
  3. 否则,根据二叉查找树的性质递归搜索.
  4. 递归返回后切记要根据堆性质旋转(这份代码用的是小根堆),假如递归时用的是 \(son[k]\),那么旋转时则要用 \(rotate_{!k}\)
  5. 记得更新(在全局写)
inline void insert(int &id,int x){
	if(!id){
		id=newnode(x);//新建
		return;
	}
	if(x==t[id].w) t[id].cnt++;//找到了
	else{
		bool k=(x>=t[id].w);
		insert(t[id].son[k],x);//递归插入
		if(t[id].data<t[t[id].son[k]].data){//按堆性质旋转
			rotate(id,k^1);//注意这里是^1
		}
	}
	update(id);
}

3.2.5 删除

  1. 未找到直接返回
  2. 否则,如果找到了,首先看 \(cnt\) 能不能减(记得更新),不能的话就把它换到最下面再删掉
  3. 删除节点的步骤是:先判断有没有孩子,没有直接删,否则将它孩子中较小的换上来(这里用的就是随机赋的那个值了,换比较小的是因为要维护小根堆性质).
  4. 否则就是进一步在子树里找,还是根据二叉查找树性质. 回来别忘了更新
inline void remove(int &id,int x){
	if(!id) return;//未找到
	if(t[id].w==x){//找到了
		if(t[id].cnt>1){
			t[id].cnt--;
			update(id);
			return;
		}
		if(t[id].son[0] or t[id].son[1]){
			if(!t[id].son[1] or t[t[id].son[0]].data>t[t[id].son[1]].data){
				rotate(id,1);//只有左儿子,或者左儿子更小
				remove(t[id].son[1],x);//转下来了直接递归即可
			}
			else{
				rotate(id,0);//否则转右儿子
				remove(t[id].son[0],x);
			}				
            update(id);			
        }
        else{
			id=0;//没有儿子
		}
		return;
	}
	(x<t[id].w)?remove(t[id].son[0],x):remove(t[id].son[1],x);//递归删除
    update(id);
}

3.2.6 查询元素排名

  1. 没找到就返回 \(1\)(具体看怎么定义了,在这里返回 \(1\) 是因为我们递归需要).
  2. 找到了就返回它左子树的 \(size\) 加上 \(1\) (这里这个 \(1\) 也按需添加)
  3. 然后就是分情况找了,假如在左子树那就直接返回答案,在右子树还要加上该节点 \(cnt\) 和左子树的 \(size\)
inline int getrank(int id,int x){
	if(!id){
		return 1;//没找到
	}
	if(x==t[id].w){
		return t[t[id].son[0]].size+1;//当前值
	}
	else if(x<t[id].w){
		return getrank(t[id].son[0],x);//左子树
	}
	else{
		return t[t[id].son[0]].size+t[id].cnt+getrank(t[id].son[1],x);//右子树
	}
}

3.2.7 查询元素值

  1. 没找到返回无解
  2. 否则还是分类讨论,通过 \(rank\) 和左子树 \(size\) 比一下可以判断是不是在左子树,再通过判断 \(rank\) 和左子树 \(size\) 加上当前 \(cnt\) 的关系来判断是不是在当前节点,还不是就要搜右子树了. 注意搜右子树的时候记得要给 \(rank\) 减去左边这些值.
inline int getval(int id,int rank){
	if(!id) return inf;//没找到
	if(rank<=t[t[id].son[0]].size){
		return getval(t[id].son[0],rank);//左子树
	}
	else if(rank<=t[t[id].son[0]].size+t[id].cnt){
		return t[id].w;//当前值
	}
	else{
		return getval(t[id].son[1],rank-t[t[id].son[0]].size-t[id].cnt);//右子树
	}
}

3.2.8 查找前驱与后继

其实这个倒是没啥好说的,按二叉搜索树性质一直跳就行了,要注意的还是跳到头及时退出.

inline int getpre(int x){//前驱
	int id=root,pre=0;
	while(id){
		if(t[id].w<x){
			pre=t[id].w;
			id=t[id].son[1];
		}
		else{
			id=t[id].son[0];
		}
	}
	return pre;
}
inline int getnext(int x){//后继
	int id=root,next=0;
	while(id){
		if(t[id].w>x){
			next=t[id].w;
			id=t[id].son[0];
		}
		else{
			id=t[id].son[1];
		}
	}
	return next;
}

3.3 完整代码

#include<bits/stdc++.h>
using namespace std;
namespace balanced_tree{
	const int N=100001,inf=114514191;
	class treap{
	private:
		int tot;
		struct tree{
			int w,data,size,cnt,son[2];
		}t[N];
	public:
		int root;
		inline void update(int x){
			t[x].size=t[t[x].son[0]].size+t[t[x].son[1]].size+t[x].cnt;
		}
		inline int newnode(int val){
			t[++tot]={val,rand(),1,1,{0,0}};
			return tot;
		}
		inline void rotate(int &id,int isrignt){
			bool k=isrignt;
			int temp=t[id].son[k^1];
			t[id].son[k^1]=t[temp].son[k];
			t[temp].son[k]=id;
			id=temp;
			update(t[id].son[k]);
			update(id);
		}
		inline void insert(int &id,int x){
			if(!id){
				id=newnode(x);
				return;
			}
			if(x==t[id].w) t[id].cnt++;
			else{
				bool k=(x>=t[id].w);
				insert(t[id].son[k],x);
				if(t[id].data<t[t[id].son[k]].data){
					rotate(id,k^1);
				}
			}
			update(id);
		}
		inline void remove(int &id,int x){
			if(!id) return;
			if(t[id].w==x){
				if(t[id].cnt>1){
					t[id].cnt--;
					update(id);
					return;
				}
				if(t[id].son[0] or t[id].son[1]){
					if(!t[id].son[1] or t[t[id].son[0]].data>t[t[id].son[1]].data){
						rotate(id,1);
						remove(t[id].son[1],x);
					}
					else{
						rotate(id,0);
						remove(t[id].son[0],x);
					}
					update(id);
				}
				else{
					id=0;
				}
				return;
			}
			(x<t[id].w)?remove(t[id].son[0],x):remove(t[id].son[1],x);
			update(id);
		}
		inline int getrank(int id,int x){
			if(!id){
				return 1;
			}
			if(x==t[id].w){
				return t[t[id].son[0]].size+1;
			}
			else if(x<t[id].w){
				return getrank(t[id].son[0],x);
			}
			else{
				return t[t[id].son[0]].size+t[id].cnt+getrank(t[id].son[1],x);
			}
		}
		inline int getval(int id,int rank){
			if(!id) return inf;
			if(rank<=t[t[id].son[0]].size){
				return getval(t[id].son[0],rank);
			}
			else if(rank<=t[t[id].son[0]].size+t[id].cnt){
				return t[id].w;
			}
			else{
				return getval(t[id].son[1],rank-t[t[id].son[0]].size-t[id].cnt);
			}
		}
		inline int getpre(int x){
			int id=root,pre=0;
			while(id){
				if(t[id].w<x){
					pre=t[id].w;
					id=t[id].son[1];
				}
				else{
					id=t[id].son[0];
				}
			}
			return pre;
		}
		inline int getnext(int x){
			int id=root,next=0;
			while(id){
				if(t[id].w>x){
					next=t[id].w;
					id=t[id].son[0];
				}
				else{
					id=t[id].son[1];
				}
			}
			return next;
		}
	};
}
using namespace balanced_tree;

4.FHQTreap

这个东西有种暴力数据结构的美,所以很好写,但是常数大,比较推荐

4.1 算法思想

把树有序地拆开,执行完了再装上,同时用 Tree+Heap 平衡

那么首先我们就要讲的是把树有序地拆开

因为显然二叉搜索树已经有序了,我们只需要决定在哪断就行了,首先从根节点开始搜起,假如根节点小于这个值,说明整个左子树都小于这个值,那就把左子树全塞进去,然后递归右子树. 否则就递归左子树,这很无脑.

拆开之后就很方便了,假如我们要插入一个数 \(x\),那么显然需要先按 \(x\) 来把树拆开,然后在端点处直接拼一下,再直接拼上就行了(其实这里用普通二叉搜索树的方法插入元素比较快,但是这么做明显比较简单,你都用 FHQ 了还要什么速度),删除也是同理.

至于拼上也比较简单,因为两颗子树一定是有序的,所以只需要判断哪一棵子树符合我们定义的堆条件,然后粘上就行了.

显然这样做还没提到平衡树的平衡,既然这东西都叫 Treap 了,显然也是用一样的思路去维护,

posted @ 2024-06-28 21:41  HaneDaniko  阅读(37)  评论(3编辑  收藏  举报