平衡树

二叉搜索树(Binary Search tree,BST)

二叉搜索树是所有平衡树的基础

定义

二叉搜索树是一种树状结构,并满足BST性质

  1. 空树是一颗二叉搜索树。
  2. 若左子树不为空,则左子树上的所有节点的权值都比根节点小。
  3. 若右子树不为空,则右子树上的所有节点的权值都比根节点大。
  4. 二叉搜索树的左右子树均满足上述性质。

用途

由于二叉搜索树满足BST性质,可以做到快速添加元素、查询元素,查询元素排名,求前驱/后继,删除元素等。

实现

建树(build)

使用结构体struct保存每个节点的左子树、右子树,以及该节点的权值,结构体外保存节点数,根
如果你觉得结构体太垃圾也可以用数组

struct BST{
	int l,r;//左右子树
	int val;  //权值
}T[NUM];
int stot,rot  // 总结点数,根节点

当需要建立新节点时:

int New(int val){
	T[++stot].val=val;
	return stot;
}

为避免越界,通常在建树时预先建立两个节点 \(-inf\) 以及 \(inf\),分别代表极小值极大值
下面是建树的实现:

struct BST{
	int l,r;
	int val;
}T[NUM];
int stot,root;
int New(int val){
	T[++stot].val=val;
	return stot;
}
void build(){
	root=New(-inf);
	T[root].r=New(inf);
}

建完的树长这样:
BST_build

插入(insert)

假设要插入权值为 \(val\) 的节点:

  1. 若当前节点为空,新建一个节点。
  2. \(val\) 大于当前节点权值,前往右子树。
  3. \(val\) 小于当前节点权值,前往左子树。

实现时引用根节点,新建一个节点后其父节点信息可以同步更改:

void insert(int val,int &p=root){
	if(!p) return p=New(val),void();
	if(val==T[p].val) return;
	if(val>T[p].val) insert(val,T[p].r);
	else insert(val,T[p].l);
}

调用入口:

insert(val);

查找(find)

假设要查找权值为 \(val\) 的节点:

  1. 若当前节点权值等于 \(val\),则返回当前节点。
  2. 若不存在当前节点,说明权值为 \(val\) 的节点不存在。
  3. \(val\) 大于当前节点权值,查找右子树。
  4. \(val\) 小于当前节点权值,查找左子树。

思路非常简单,代码也是:

int find(int val,int p=root){
	if(!p) return -inf;
	if(val==T[p].val) return p;
	return val>T[p].val?find(val,T[p].r):find(val,T[p].l);
}

调用入口:

find(val);

前驱/后继(prev/next)

定义

\(val\) 的前驱: 在二叉搜索树中,权值小于 \(val\) 的最大权值。
\(val\) 的后继: 在二叉搜索树中,权值大于 \(val\) 的最小权值。

求法

前驱(prev)
\(val\) 的前驱为 \(ans\)

  1. 先让 \(ans\)\(-inf\) 的节点,然后查找 \(val\)
  2. 在查找过程中,若当前节点的权值小于 \(val\) 那么说明当前节点可能成为 \(ans\),这时候将 \(val\)\(ans\) 所对应的权值比较,取最大值。
  3. 若查找到 \(val\),且其对应的节点有左子树,那么 \(ans\) 就应该为其左子树中最靠右的点(即权值最大的点)。
  4. 若没有左子树或没找到 \(val\) 对应的节点,此时的 \(ans\) 即为答案。

后继(next)
与求前驱差不多,设 \(val\) 的前驱为 \(ans\)

  1. 先让 \(ans\)\(inf\) 的节点,然后查找 \(val\)
  2. 在查找过程中,若当前节点的权值大于于 \(val\) 那么说明当前节点可能成为 \(ans\),这时候将 \(val\)\(ans\) 所对应的权值比较,取最小值。
  3. 若查找到 \(val\),且其对应的节点有右子树,那么 \(ans\) 就应该为其右子树中最靠左的点(即权值最小的点)。
  4. 若没有右子树或没找到 \(val\) 对应的节点,此时的 \(ans\) 即为答案。

下面解释一下

以求前驱为例:

我们可以发现,一个节点的右子树上的点的权值肯定比当前的大,而前驱的定义是权值小于 \(val\) 的最大权值,那么当前节点一定是 \(ans\) 的一个候选,这时应当将当前节点的权值与 \(ans\) 进行比较,取权值更大的一项。
当找到 \(val\) 时,若当前节点有左子树,为什么答案就是左子树中最靠右的点呢?想一下为什么这点能成为左子树?因为它比 \(val\) 小。为什么能成为左子树中最靠右的点?因为它是这一堆权值比 \(val\) 小的节点中最大的一个,这不就是 \(val\) 的前驱了吗!

代码

int getpre(int val){
	int ans=1,p=root;
	while(p){
		if(val==T[p].val){
			if(T[p].l>0){
				p=T[p].l;
				while(T[p].r>0) p=T[p].r;
				ans=p;
			}
			break;
		}
		if(T[p].val<val&&T[p].val>T[ans].val) ans=p;
		p=val<T[p].val?T[p].l:T[p].r;
	}
	return ans;
}
int getnext(int val){
	int ans=2,p=root;
	while(p){
		if(val==T[p].val){
			if(T[p].r>0){
				p=T[p].r;
				while(T[p].l>0) p=T[p].l;
				ans=p;
			}
			break;
		}
		if(T[p].val>val&&T[p].val<T[ans].val) ans=p;
		p=val<T[p].val?T[p].l:T[p].r;
	}
	return ans;
}

为什么这个代码长得这么丑
注意这里我们返回的是节点的编号!!!

删除(remove)

删除比较麻烦,因为删除一个节点后还得保证这是一个二叉搜索树,具体步骤如下:
设要删除权值为 \(val\) 的节点。

  1. 先查找 \(val\),记为 \(p\);
  2. \(p\) 的子节点数小于等于 \(1\),直接删除,并令其子节点替代其位置。
  3. \(p\) 的子节点数大于 \(1\),查找 \(val\) 的后继 \(next\) ,令 \(next\) 的右子树替代\(next\) 位置,令 \(next\) 替代 \(p\),删除 \(p\)

可以证明:若 \(p\) 的子节点数大于 \(1\),那么后继 \(next\) 没有左子树,否则此 \(next\) 不是 \(val\) 的后继。

void remove(int val,int &p=root){
	if(!p) return;
	if(val==T[p].val){
		if(T[p].l==0){
			p=T[p].r;
		}else if(T[p].r==0){
			p=T[p].l;
		}else{
			int nxt=T[p].r;
			while(T[nxt].l) nxt=T[nxt].l;
			remove(T[nxt].val,T[p].r);
			T[nxt].l=T[p].l,T[nxt].r=T[p].r;
			p=nxt;
		}
		return;
	}
	if(val>T[p].val) remove(val,T[p].r);
	else remove(val,T[p].l);
}

\(val\) 的排名(getrank)

需要在结构体中额外维护一个值 \(siz\) 表示以当前节点为根的子树的节点个数,那么我们可以写出pushup函数:

void pushup(int p){
	T[p].size=T[T[p].l].size+T[T[p].r].size+1;
}

其中 \(+1\) 表示加上当前节点大小。
每次进行有关节点的添加、删除操作后,对根(当前子树的根)进行pushup即可。

于是我们就可以写出查询排名的操作步骤:
当到达一个节点 \(p\)

  1. \(p\) 不存在,返回 \(0\)(有时需要返回 \(1\))。
  2. 如果 \(val\) 大于 \(p\) 的权值,说明 \(p\) 的左子树的权值都比 \(val\) 小,答案要加上 \(p\) 左子树的 \(siz\) 以及 \(p\) 这一节点,继续向 \(p\) 的右子树查找 \(val\)
  3. 如果 \(val\) 小于 \(p\) 的权值,无法确定是否能对答案造成贡献,继续向左子树查找 \(val\)
  4. 若查找到 \(val\),返回 \(p\) 左子树的 \(siz+1\)

关于 \(p\) 不存在返回 \(1\) 还是 \(0\) 的问题,假设 \(val\) 不存在于树中:

  • 返回 \(1\) 表示在这颗树中第一个\(val\) 的值的排名。
  • 返回 \(0\) 表示在这颗树中第一个\(val\) 的值的排名。
int getrank(int val,int p=root){
	if(!p) return 0;
	if(T[p].val==val) return T[T[p].l].size+1;
	if(T[p].val>val) return getrank(val,T[p].l);
	return T[T[p].l].size+1+getrank(val,T[p].r);
}

有不懂的看着看着就懂了
还要注意一点不要忘了有一个权值为 \(-inf\) 的节点!!!

查排名为 \(rank\) 的值(getval)

类似查排名。
当到达一个节点 \(p\) ,记其左子树大小为 \(siz_l\)

  1. \(p\) 不存在返回一个极大值(不存在的排名)。
  2. \(siz_l\) 大于等于 \(rank\),继续在左子树中查找排名为 \(rank\) 的值。
  3. \(siz_l+1\) 等于 \(rank\),返回 \(p\) 的权值,即为答案。
  4. \(siz_l\) 小于 \(rank\),继续在右子树中查找排名为 \(rank-siz_l-1\) 的节点。
int getval(int rank,int p=root){
	if(p==0) return INF;
	if(T[T[p].l].size>=rank) return getval(rank,T[p].l);
	if(T[T[p].l].size+1==rank) return T[p].val;
	return getval(rank-T[T[p].l].size-1,T[p].r);
}

还要注意一点不要忘了有一个权值为 \(-inf\) 的节点!!!

对于可重集合

可以额外维护一个 \(cnt\),记录每个元素出现的次数。
下面是一些更改:

void pushup(int p){
	T[p].size=T[T[p].l].size+T[T[p].r].size+T[p].cnt;
}
int getrank(int val,int p=root){
	if(!p) return 0;
	if(T[p].val==val) return T[T[p].l].size+1;
	if(T[p].val>val) return getrank(val,T[p].l);
	return T[T[p].l].size+T[p].cnt+getrank(val,T[p].r);
}
int getval(int rank,int p=root){
	if(p==0) return INF;
	if(T[T[p].l].size>=rank) return getval(rank,T[p].l);
	if(T[T[p].l].size+T[p].cnt>=rank) return T[p].val;
	return getval(rank-T[T[p].l].size-T[p].cnt,T[p].r);
}

缺点

想象一下,当我们一次将一个升序序列插入,那么我们得到的将不会是一颗树,也就是说,在特殊构造的数据中,二叉搜索树会退化为一条链。

那么对这棵树操作的复杂度就会退化到 \(O(n)\),这是我们不能接受的,这称之为这棵二叉搜索树失去了平衡

平衡树

顾名思义,平衡树就是一颗平衡的二叉搜索树,平衡树右很多种,每一种都有其独特的保持平衡的方式。

Treap(树堆/笛卡尔树)

Treap是一种特殊的笛卡尔树
Treap 是一种弱平衡二叉搜索树,又称树堆,是一种结合了二叉搜索树和堆的数据结构,它除了要维护每个点的权值(val),还要维护每个点的优先级(priority),其中权值满足二叉搜索树性质,优先级满足堆性质。
对于二叉搜索树性质,前文已经给出,堆性质指的是:

  • 子节点的优先级比父节点大或小(取决于是大根堆还是小根堆)

其中优先级随机给出,目的是通过随机化,打乱原本的不平衡形态。
虽然有点扯,但这还真有用。

struct BST{
	int l,r;
	int val,pty;
}T[NUM];
int stot,root;
int New(int val){
	T[++stot].val=val;
	T[stot].pty=rand();
	return stot;
}

有旋Treap

通过旋转维护平衡,主要分为右旋左旋

旋转(zig/zag)


记当前节点为 \(p\)
其中右旋为:

  • \(p\) 的左节点的右节点作为 \(p\) 的左节点,将 \(p\) 作为其左节点的右节点,并将 \(p\) 的左节点放在 \(p\) 的位置。

其中左旋为:

  • \(p\) 的右节点的左节点作为 \(p\) 的右节点,将 \(p\) 作为其右节点的左节点,并将 \(p\) 的右节点放在 \(p\) 的位置。

说白了就是绕口令
看不懂没关系,图看懂了就行。
下面是旋转代码:

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

代码

由于加入了旋转等操作,有旋Treap与传统二叉搜索树的写法有了些许改变:

  • 插入一个新节点时,需要判断树是否满足堆性质,否则应通过旋转来使其保持堆性质。
  • 删除一个节点时,不需要再去寻找前驱后继,只需将要删除的节点旋转到叶节点直接删除即可。

下面给出完整有旋Treap代码:

struct打包
const int NUM=1e7;
const int inf=2147483647;
struct tree{
	int l,r;
	int val,dat;
	int cnt,size;
}T[NUM];
int tot;
struct Treap{
	int root;
	int New(int val){
		T[++tot].val=val;
		T[tot].dat=rand();
		T[tot].cnt=T[tot].size=1;
		return tot;
	}
	void update(int p){
		T[p].size=T[T[p].l].size+T[T[p].r].size+T[p].cnt;
	}
	void build(int &p){
		p=New(-inf),New(inf);
		T[p].r=New(inf);
		update(p);
	}
	int getrank(int p,int val){
		if(!p) return 1;
		if(T[p].val==val) return T[T[p].l].size+1;
		if(T[p].val>val) return getrank(T[p].l,val);
		return T[T[p].l].size+T[p].cnt+getrank(T[p].r,val); 
	}
	int getval(int p,int rank){
		if(p==0) return inf;
		if(T[T[p].l].size>=rank) return getval(T[p].l,rank);
		if(T[T[p].l].size+T[p].cnt>=rank) return T[p].val;
		return getval(T[p].r,rank-T[T[p].l].size-T[p].cnt);
	}
	void zig(int &p){
		int q=T[p].l;
		T[p].l=T[q].r,T[q].r=p,p=q;
		update(T[p].r),update(p);
	}
	void zag(int &p){
		int q=T[p].r;
		T[p].r=T[q].l,T[q].l=p,p=q;
		update(T[p].l),update(p);
	}
	void insert(int &p,int val){
		if(p==0){
			p=New(val);
			return;
		}
		if(val==T[p].val){
			T[p].cnt++;
			update(p);
			return;
		}
		if(val<T[p].val){
			insert(T[p].l,val);
			if(T[p].dat<T[T[p].l].dat) zig(p);
		}else{
			insert(T[p].r,val);
			if(T[p].dat<T[T[p].r].dat) zag(p);
		}
		update(p);
	}
	int getpre(int val){
		int ans=1,p=root;
		while(p){
			if(val==T[p].val){
				if(T[p].l>0){
					p=T[p].l;
					while(T[p].r>0) p=T[p].r;
					ans=p;
				}
				break;
			}
			if(T[p].val<val&&T[p].val>T[ans].val) ans=p;
			p=val<T[p].val?T[p].l:T[p].r;
		}
		return ans;
	}
	int getnext(int val){
		int ans=2,p=root;
		while(p){
			if(val==T[p].val){
				if(T[p].r>0){
					p=T[p].r;
					while(T[p].l>0) p=T[p].l;
					ans=p;
				}
				break;
			}
			if(T[p].val>val&&T[p].val<T[ans].val) ans=p;
			p=val<T[p].val?T[p].l:T[p].r;
		}
		return ans;
	}
	void remove(int &p,int val){
		if(p==0) return;
		if(val==T[p].val){
			if(T[p].cnt>1) return T[p].cnt--,update(p),void();
			if(T[p].l||T[p].r){
				if(T[p].r==0||T[T[p].l].dat>T[T[p].r].dat){
					zig(p),remove(T[p].r,val);
				}else{
					zag(p),remove(T[p].l,val);
				}
				update(p);
			}
			else p=0;
			return;
		} 
		val<T[p].val?remove(T[p].l,val):remove(T[p].r,val);
		update(p);
	}
};
namespace打包
namespace Treap{
	const int NUM=1e6+10;
	struct tree{
		int l,r;
		int val,dat;
		int cnt,size;
	}T[NUM];
	int tot,root,INF=0x3f3f3f3f;
	int New(int val){
		T[++tot].val=val;
		T[tot].dat=rand();
		T[tot].cnt=T[tot].size=1;
		return tot;
	}
	void update(int p){
		T[p].size=T[T[p].l].size+T[T[p].r].size+T[p].cnt;
	}
	void build(){
		New(-INF),New(INF);
		root=1,T[1].r=2;
		update(root);
	}
	int getrank(int val,int p=root){
		if(!p) return 1;
		if(T[p].val==val) return T[T[p].l].size+1;
		if(T[p].val>val) return getrank(val,T[p].l);
		return T[T[p].l].size+T[p].cnt+getrank(val,T[p].r); 
	}
	int getval(int rank,int p=root){
		if(p==0) return INF;
		if(T[T[p].l].size>=rank) return getval(rank,T[p].l);
		if(T[T[p].l].size+T[p].cnt>=rank) return T[p].val;
		return getval(rank-T[T[p].l].size-T[p].cnt,T[p].r);
	}
	void zig(int &p){
		int q=T[p].l;
		T[p].l=T[q].r,T[q].r=p,p=q;
		update(T[p].r),update(p);
	}
	void zag(int &p){
		int q=T[p].r;
		T[p].r=T[q].l,T[q].l=p,p=q;
		update(T[p].l),update(p);
	}
	void insert(int val,int &p=root){
		if(p==0){
			p=New(val);
			return;
		}
		if(val==T[p].val){
			T[p].cnt++;
			update(p);
			return;
		}
		if(val<T[p].val){
			insert(val,T[p].l);
			if(T[p].dat<T[T[p].l].dat) zig(p);
		}else{
			insert(val,T[p].r);
			if(T[p].dat<T[T[p].r].dat) zag(p);
		}
		update(p);
	}
	int getpre(int val){
		int ans=1,p=root;
		while(p){
			if(val==T[p].val){
				if(T[p].l>0){
					p=T[p].l;
					while(T[p].r>0) p=T[p].r;
					ans=p;
				}
				break;
			}
			if(T[p].val<val&&T[p].val>T[ans].val) ans=p;
			p=val<T[p].val?T[p].l:T[p].r;
		}
		return ans;
	}
	int getnext(int val){
		int ans=2,p=root;
		while(p){
			if(val==T[p].val){
				if(T[p].r>0){
					p=T[p].r;
					while(T[p].l>0) p=T[p].l;
					ans=p;
				}
				break;
			}
			if(T[p].val>val&&T[p].val<T[ans].val) ans=p;
			p=val<T[p].val?T[p].l:T[p].r;
		}
		return ans;
	}
	void remove(int val,int &p=root){
		if(p==0) return;
		if(val==T[p].val){
			if(T[p].cnt>1) return T[p].cnt--,update(p),void();
			if(T[p].l||T[p].r){
				if(T[p].r==0||T[T[p].l].dat>T[T[p].r].dat){
					zig(p),remove(val,T[p].r);
				}else{
					zag(p),remove(val,T[p].l);
				}
				update(p);
			}
			else p=0;
			return;
		} 
		val<T[p].val?remove(val,T[p].l):remove(val,T[p].r);
		update(p);
	}
}
using namespace Treap;

无旋Treap(FHQ-Treap)

可以说是最简单、最好写哪里好写的平衡树,但代价就是常数大。

无旋Treap通过分裂、合并来保持平衡。

分裂(split)

无旋Treap分裂后会分裂成两个小Treap,在这里称之为左树和右树。
分裂时通常要有一个值 \(val\),将所有比 \(val\) 小或等于的节点放在左树上,将所有值比 \(val\) 大的放在右树上。
我们分别为左树右树创建一个虚拟节点 \(x\)\(y\)

  1. 若当前节点的权值小于 \(val\),则说明说明当前节点与其左子树都比 \(val\) 小,当前节点与其左子树整个放入左树的虚拟节点中,并在当前节点的右子节点重新建立左树的建立虚拟节点。
  2. 若当前节点的权值大于 \(val\),则说明说明当前节点与其右子树都比 \(val\) 大,当前节点与其左子树整个放入右树的虚拟节点中,并在当前节点的左子节点重新建立右树的建立虚拟节点。
  3. 若当前节点为空,则说明没有新的节点了,直接删除左右树的虚拟节点。

字太多会不会有人不愿意看,其实1和2是一样的
这里引用别人的图。

代码有点让人云里雾里的,第一次我也没太看懂:

void split(int &x,int &y,int val,int p=root){
	if(!p) return x=y=0,void();
	if(T[p].val<=val) x=p,split(T[p].r,y,val,T[p].r);
	else y=p,split(x,T[p].l,val,T[p].l);
	pushup(p);
}

不会只有我把根写在了最后一个吧
稍微解释一下,这个实现比较神奇,它其实并没有建立一个建立一个虚拟节点,而是将 \(p\) 的子节点引用,当要在其上面加树的时候直接让其指向被加树的根节点,如果还不理解的话可以手动模拟一下。

合并(merge)

合并就比较简单了,设要讲以 \(u\) 为根的左树与以 \(v\) 为根的右树合并:

  1. \(u\) 的优先级比 \(v\) 小,将 \(v\) 合并到 \(u\) 的右子树。
  2. \(u\) 的优先级比 \(v\) 大,将 \(u\) 合并到 \(v\) 的左子树。

以上是小根堆的策略,大根堆反过来就行了。

int merge(int u,int v){
	if(!u||!v) return u+v;
	if(T[u].pty<T[v].pty){
		T[u].r=merge(T[u].r,v);
		return pushup(u),u; 
	}else{
		T[v].l=merge(u,T[v].l);
		return pushup(v),v;
	}
}

代码

为什么说无旋Treap是最好写的平衡树?
有了分裂和合并操作,插入、删除、前驱、后继、排名等操作就会变得非常简单,下面以插入为例:

我们不在需要递归找到 \(val\) 的位置,只需要将整个树按 \(val\) 拆开,将左树与 \(val\) 合并,再让左树与右树合并即可。
这就导致了无旋Treap的 \(log\) 奇大无比

void insert(int val,int &p=root){
	int x,y;
	split(x,y,val,p);
	p=merge(merge(x,New(val)),y);
}

其他操作同理,但查找排名还得按之前的方法。
下面是完整无旋Treap代码:

struct打包
const int NUM=1e7;
const int inf=2147483647;
struct tree{
	int l,r;
	int val,pty;
	int siz,cnt;
}T[NUM];
int stot;
struct Treap{
	int root;
	void pushup(int p){
		T[p].siz=T[T[p].l].siz+T[T[p].r].siz+T[p].cnt;
	}
	void split(int p,int val,int &x,int &y){
		if(!p) return x=y=0,void();
		if(T[p].val<=val) x=p,split(T[p].r,val,T[p].r,y);
		else y=p,split(T[p].l,val,x,T[p].l);
		pushup(p);
	}
	int merge(int x,int y){
		if(!x||!y) return x|y;
		if(T[x].pty<T[y].pty){
			T[x].r=merge(T[x].r,y);
			return pushup(x),x;
		}else{
			T[y].l=merge(x,T[y].l);
			return pushup(y),y;
		}
	}
	int New(int val){
		T[++stot].val=val;
		T[stot].pty=rand();
		T[stot].siz=T[stot].cnt=1;
		return stot;
	}
	void insert(int val){
		int x,y,z;
		split(root,val,x,z);
		split(x,val-1,x,y);
		if(T[y].cnt) T[y].cnt++,T[y].siz++,root=merge(merge(x,y),z);
		else root=merge(merge(x,New(val)),z);
	}
	void build(){
		insert(-inf),insert(inf);
	}
	int find(int p,int val){
		if(!p) return 0;
		if(val==T[p].val) return p;
		return val>T[p].val?find(T[p].r,val):find(T[p].l,val);
	}
	int getval(int p,int rank){
		if(!p) return inf;
		if(T[T[p].l].siz>=rank) return getval(T[p].l,rank);
		if(T[T[p].l].siz+T[p].cnt>=rank) return T[p].val;
		return getval(T[p].r,rank-T[T[p].l].siz-T[p].cnt);
	}
	int getrank(int val){
		int x,y;
		split(root,val-1,x,y);
		int k=T[x].siz+1;
		root=merge(x,y);
		return k;
	}
	int getpre(int val){
		int x,y;
		split(root,val-1,x,y);
		int ans=getval(x,T[x].siz);
		root=merge(x,y);
		return ans;
	}
	int getnext(int val){
		int x,y;
		split(root,val,x,y);
		int ans=getval(y,1);
		root=merge(x,y);
		return ans;
	}
	void remove(int val){
		int x,y,z;
		split(root,val,x,z);
		split(x,val-1,x,y);
		if(T[y].cnt>1) T[y].cnt--,T[y].siz--;
		else y=merge(T[y].l,T[y].r);
		root=merge(merge(x,y),z);
	}
};
namespace打包
namespace Treap{
	const int NUM=1e6+10; 
	const int inf=0x3f3f3f3f;
	struct tree{
		int l,r;
		int val,pty;
		int siz,cnt;
	}T[NUM];
	int root,stot;
	void pushup(int p){
		T[p].siz=T[T[p].l].siz+T[T[p].r].siz+T[p].cnt;
	}
	void split(int &x,int &y,int val,int p=root){
		if(!p) return x=y=0,void();
		if(T[p].val<=val) x=p,split(T[p].r,y,val,T[p].r);
		else y=p,split(x,T[p].l,val,T[p].l);
		pushup(p);
	}
	int merge(int u,int v){
		if(!u||!v) return u+v;
		if(T[u].pty<T[v].pty){
			T[u].r=merge(T[u].r,v);
			return pushup(u),u; 
		}else{
			T[v].l=merge(u,T[v].l);
			return pushup(v),v;
		}
	}
	int New(int val){
		T[++stot].val=val;
		T[stot].pty=rand();
		T[stot].cnt=T[stot].siz=1;
		return stot;
	}
	void insert(int val,int &p=root){
		int x,y,z;
		split(x,z,val,p);
		split(x,y,val-1,x);
		if(y) T[y].cnt++,T[y].siz++,p=merge(merge(x,y),z);
		else p=merge(merge(x,New(val)),z);
	}
	void build(int &p=root){
		insert(-inf,p),insert(inf,p);
	}
	int find(int val,int p=root){
		if(!p) return 0;
		if(val==T[p].val) return p;
		return val>T[p].val?find(val,T[p].r):find(val,T[p].l);
	}
	int getval(int rank,int p=root){
		if(p==0) return inf;
		if(T[T[p].l].siz>=rank) return getval(rank,T[p].l);
		if(T[T[p].l].siz+T[p].cnt>=rank) return T[p].val;
		return getval(rank-T[T[p].l].siz-T[p].cnt,T[p].r);
	}
	int getrank(int val,int &p=root){
		int x,y;
		split(x,y,val-1,p);
		int k=T[x].siz+1;
		p=merge(x,y);
		return k;
	}
	int getpre(int val,int &p=root){
		int x,y;
		split(x,y,val-1,p);
		int tmp=getval(T[x].siz,x);
		p=merge(x,y);
		return tmp;
	}
	int getnext(int val,int &p=root){
		int x,y;
		split(x,y,val,p);
		int tmp=getval(1,y);
		p=merge(x,y);
		return tmp;
	}
	void remove(int val,int &p=root){
		int x,y,z;
		split(x,z,val,p);
		split(x,y,val-1,x);
		if(T[y].cnt>1) T[y].cnt--,T[y].siz--,p=merge(merge(x,y),z);
		else y=merge(T[y].l,T[y].r),p=merge(merge(x,y),z);
	}
}
using namespace Treap;

Splay树(伸展树)

Splay 是一种自平衡二叉查找树,由 Daniel Sleator 和 Robert Tarjan 没错又是他与1985年发明,它用过伸展操作不断将某个节点旋转到根节点来保持平衡。

旋转(rotate)

为什么不是左旋和右旋?
其实就是左旋右旋,只是Splay在在实现的时候将这俩打包成了一个函数,于是就可以理解为将一个节点转上去(转到其父节点的位置)所以是不是可以叫上旋,这就需要额外维护父节点信息 \(fa\) ,以及为了方便,将左右子节点放在了数组中维护(方便取下标)。

struct tree{
	int s[2],f;
	int val,siz,cnt;
}T[NUM];

记当前节点为 \(x\)\(y\)\(x\) 的父节点,\(z\)\(y\) 的父节点:

  1. \(x\) 挂在 \(z\) 上。
  2. \(x\) 的子节点挂在 \(y\) 上。
  3. \(y\) 挂在 \(x\) 上。

云里雾里!我到底在写什么
我们一条一条说:

\(x\) 挂在 \(z\) 上:
显然不知道哪条边挂在哪条边上

我们得先知道 \(x\) 要挂在 \(z\) 的那个节点上,要挂在 \(z\)\(y\) 的那个地方,那么我们怎么能知道 \(y\) 是在 \(z\) 的左边还是右边呢?
还记得前面说过的一句话吗:

为了方便,将左右子节点放在了数组中维护(方便取下标)。

于是聪明的你就想到了一个绝妙的方法:

  T[z].s[1]==y;

这是一个bool类型的表达式,看看如果 \(y\)\(z\) 的右节点,这个式子的值是什么?\(1\)!那如果 \(y\)\(z\) 的左节点呢?\(0\)!这不就是 \(y\) 所在的节点吗恍然大悟
这样就可以写出来第一条的代码:

  T[z].s[T[z].s[1]==y]=x;
  T[x].f=z;

\(x\) 的子节点挂在 \(y\) 上:
显然x哪个子节点挂在y上

这个问题其实很好想:

  • 如果 \(x\)\(y\) 的左节点,那么就将 \(x\) 的右节点挂在 \(y\) 的左节点上。
  • 如果 \(x\)\(y\) 的右节点,那么就将 \(x\) 的左节点挂在 \(y\) 的右节点上。

我们记 \(k\)\(x\)\(y\) 节点的位置,\(k=0\) 表示 \(x\)\(y\) 的左节点, \(x=1\) 表示 \(x\)\(y\) 的右节点,于是我们可以写出第二部分代码:

  T[y].s[k]=T[x].s[k^1];
  T[T[x].s[k^1]].f=y;

\(y\) 挂在 \(x\) 上:

这个简单,\(x\) 的哪个节点挂在了 \(y\) 上,就 \(y\) 挂在哪个节点上:

  T[x].s[k^1]=y;
  T[y].f=x;

下面是旋转部分的代码:

void rotate(int x){
	int y=T[x].f;
	int z=T[y].f;
	int k=T[y].s[1]==x;
	T[z].s[T[z].s[1]==y]=x;
	T[x].f=z;
	T[y].s[k]=T[x].s[k^1];
	T[T[x].s[k^1]].f=y;
	T[x].s[k^1]=y;
	T[y].f=x;
	pushup(y),pushup(x);
} 

伸展(splay)

伸展便是Splay的精髓其实也是旋转,说人话就是将要查的节点直接转到根节点然后就可以为所欲为了
伸展时需要考虑两种情况:

  1. 一字形:
    \(x\)\(y\)\(z\) 程一字型分布时,先将 \(y\) 转到 \(z\) 的位置,再将 \(x\) 转上来。
  2. 之字形:
    \(x\)\(y\)\(z\) 程之字型分布时,先将 \(x\) 转到 \(y\) 的位置,再将 \(x\) 转上来。

代码实现过程中,若要将 \(x\) 转到 \(k\) 的子节点位置,特别地,若 \(k=0\) 则是旋转到根节点,可以不断地取 \(x\) 的父节点 \(y\) 与祖父节点 \(z\)\(z\) 不是 \(k\) 就按上述方式进行旋转,否则就直接将 \(x\) 旋转到 \(y\)
最后,如果 \(k=0\) 记得把根换成 \(x\)

void splay(int x,int k){
	while(T[x].f!=k){
		int y=T[x].f;
		int z=T[y].f;
		if(z!=k){
			if((T[y].s[1]==x)^(T[z].s[1]==y)){
				rotate(x);
			}else rotate(y);
		}
		rotate(x);
	}
	if(!k) root=x;
}

以下未完工......

posted @ 2026-07-10 12:01  LZYXT  阅读(30)  评论(0)    收藏  举报