平衡树

前言

Updating...(咕咕咕)

两年后……博主实在懒得更了 qwq

这篇博客不会再更了,但是 TOC 还留下,读者可以查找相关资料 .

因为两年前写的东西,这里也是比较入门的平衡树,更深入的理解还是多靠积累吧 .

UOJ 群里大佬多哦,甚至还有人说任何平衡树都可以持久化???

好吧,前言没了,看看文章吧 .

前置芝士

  1. 二叉搜索树(Binary Search Tree,BST)
  2. 堆(Heap)(Treap 需要)

BST 是最好的平衡树!只要数据随机吊打一切其他平衡树!

下面大部分平衡树都是不把相同元素合在一起的,为了防止毒瘤出题人出毒瘤数据卡可以自行随机合并(特别麻烦)或者合点(容易)

普通平衡树

模板:

普通平衡树的基本操作可以参考上方模板

1. 替罪羊树

众所周知 每种平衡树都有一种操作来维护平衡。

替罪羊树是暴力维护平衡的(

替罪羊树要存 左右儿子,节点值,子树实际大小(前面的一般平衡树都要用),子树不实际大小(?)还有删除标记(后面解释)。

struct Node
{
	int l,r,val;
	int size,fact; // 实际大小:fact
	bool exist; // exist : 是否存在
}tzy[N];
int cnt,root; // cnt : 节点数量    root : 根

// 创建新节点
inline void newnode(int &now,int val){now=++cnt; tzy[now].val=val; tzy[now].size=tzy[now].fact=1; tzy[now].exist=true;}

1.1. 插入

只要按 BST 插入方法插入即可,但是为了平衡还得在后面加一句判平衡。

void ins(int &now,int val)
{
	if (!now){newnode(now,val); check(root,now); return ;} // check : 判平衡
	++tzy[now].size; ++tzy[now].fact;
	if (val<tzy[now].val) ins(tzy[now].l,val);
	else ins(tzy[now].r,val);
}

1.2. 删除

还记得前面节点里有维护「删除标记」吗?

对,删除就是把节点打上标记。

而「子树不实际大小」就是树中子树的大小,
「子树实际大小」就是树中子树去掉打了标记的大小。

同样,为了平衡还得在后面加一句判平衡。

void del(int now,int val)
{
	if (tzy[now].exist&&tzy[now].val==val){tzy[now].exist=false; tzy[now].fact--; check(root,now); return ;}
	tzy[now].fact--;
	if (val<tzy[now].val) del(tzy[now].l,val);
	else del(tzy[now].r,val);
}

!.1 判断树是否平衡并调整树

插入和删除都提到了「判平衡」,但是「判平衡」怎么写呢?

我们从根向要操作的节点找,如果找到了需要重构的节点,就暴力重构它的子树。

判断是否平衡的条件是:

当前节点的左子树或右子树的大小大于当前节点的大小 ×α,其中 α 是平衡因子,或
当前节点为根的子树内被删除的节点数量 > 树大小(非实际)的 30% .

α 必须取 [0.5,1],一般取 α[0.7,0.8],最平常的是取 α=0.75 .

inline bool imbalence(int now){return (max(tzy[tzy[now].l].size,tzy[tzy[now].r].size)>tzy[now].size*alpha)||
                                      (tzy[now].size-tzy[now].fact>tzy[now].size*0.3);                      }

所以总体的 check 大概是这样的:

void check(int &now,int end)
{
	if (now==end) return;
	if (imbalence(now)){rebuild(now); update(root,now); return ;}
	if (tzy[end].val<tzy[now].val) check(tzy[now].l,end);
	else check(tzy[now].r,end);
}

这里 rebuild 是重构,update 是更新数据(size)。

update 非常的好写:

void update(int now,int end)
{
	if (!now) return;
	if (tzy[end].val<tzy[now].val) update(tzy[now].l,end);
	else update(tzy[now].r,end);
	tzy[now].size=tzy[tzy[now].l].size+tzy[tzy[now].r].size+1;
}

前面说过了,替罪羊树的维护平衡(重构)是暴力,替罪羊树的重构就是拉开(中序遍历)再拎起来。

来个动图演示一下:
rPeTqH.gif

vector<int> v;
void ldr(int now) // 拉开
{
	if (!now) return;
	ldr(tzy[now].l);
	if (tzy[now].exist) v.push_back(now);
	ldr(tzy[now].r);
}
void lift(int l,int r,int &now) // 拎起来
{
	if (l==r){now=v[l]; tzy[now].l=tzy[now].r=0; tzy[now].size=tzy[now].fact=1; return ;}
	int m=(l+r)>>1;
	while ((l<m)&&(tzy[v[m]].val==tzy[v[m-1]].val)) --m;
	now=v[m];
	if (l<m) lift(l,m-1,tzy[now].l);
	else tzy[now].l=0;
	lift(m+1,r,tzy[now].r);
	tzy[now].size=tzy[tzy[now].l].size+tzy[tzy[now].r].size+1; // (
	tzy[now].fact=tzy[tzy[now].l].fact+tzy[tzy[now].r].fact+1;
}
void rebuild(int &now)
{
	v.clear(); ldr(now);
	if (v.empty()){now=0; return ;}
	lift(0,v.size()-1,now);
}

1.3. & 1.4. 查询值的排名 / 查询排名的值

这些就按照普通 BST 写就行啦

int getrank(int val)
{
	int now=root,rank=1;
	while (now)
	{
		if (val<=tzy[now].val) now=tzy[now].l;
		else {rank+=tzy[now].exist+tzy[tzy[now].l].fact; now=tzy[now].r;}
	} return rank;
}
int getval(int rank)
{
	int now=root;
	while (now)
	{
		if (tzy[now].exist&&tzy[tzy[now].l].fact+tzy[now].exist==rank) break;
		else if (tzy[tzy[now].l].fact>=rank) now=tzy[now].l;
		else {rank-=tzy[tzy[now].l].fact+tzy[now].exist; now=tzy[now].r;} // 这个 rank-=... 有主席树内味了
	} return tzy[now].val;
}

1.5. & 1.6. 求前趋 / 求后继

前趋和后继有个小技巧:
前趋:getval(getrank(x)-1);
后继:getval(getrank(x+1));

inline int pre(int x){return getval(getrank(x)-1);}
inline int nxt(int x){return getval(getrank(x+1));}

完整代码


struct tzy
{
private:
	static constexpr double alpha=0.75;
	struct Node
	{
		int l,r,val;
		int size,fact;
		bool exist;
	}tr[N];
	int cnt,root;
	inline void newnode(int &now,int val){now=++cnt; tr[now].val=val; tr[now].size=tr[now].fact=1; tr[now].exist=true;}
	inline bool imbalence(int now){return (max(tr[tr[now].l].size,tr[tr[now].r].size)>tr[now].size*alpha)||
	                                      (tr[now].size-tr[now].fact>tr[now].size*0.3);                      }
	vector<int> v;
	void ldr(int now)
	{
		if (!now) return;
		ldr(tr[now].l);
		if (tr[now].exist) v.push_back(now);
		ldr(tr[now].r);
	}
	void lift(int l,int r,int &now)
	{
		if (l==r){now=v[l]; tr[now].l=tr[now].r=0; tr[now].size=tr[now].fact=1; return ;}
		int m=(l+r)>>1;
		while ((l<m)&&(tr[v[m]].val==tr[v[m-1]].val)) --m;
		now=v[m];
		if (l<m) lift(l,m-1,tr[now].l);
		else tr[now].l=0;
		lift(m+1,r,tr[now].r);
		tr[now].size=tr[tr[now].l].size+tr[tr[now].r].size+1;
		tr[now].fact=tr[tr[now].l].fact+tr[tr[now].r].fact+1;
	}
	void rebuild(int &now)
	{
		v.clear(); ldr(now);
		if (v.empty()){now=0; return ;}
		lift(0,v.size()-1,now);
	}
	void update(int now,int end)
	{
		if (!now) return;
		if (tr[end].val<tr[now].val) update(tr[now].l,end);
		else update(tr[now].r,end);
		tr[now].size=tr[tr[now].l].size+tr[tr[now].r].size+1;
	}
	void check(int &now,int end)
	{
		if (now==end) return;
		if (imbalence(now)){rebuild(now); update(root,now); return ;}
		if (tr[end].val<tr[now].val) check(tr[now].l,end);
		else check(tr[now].r,end);
	}
	void ins(int &now,int val)
	{
		if (!now){newnode(now,val); check(root,now); return ;}
		++tr[now].size; ++tr[now].fact;
		if (val<tr[now].val) ins(tr[now].l,val);
		else ins(tr[now].r,val);
	}
	void del(int now,int val)
	{
		if (tr[now].exist&&tr[now].val==val){tr[now].exist=false; tr[now].fact--; check(root,now); return ;}
		tr[now].fact--;
		if (val<tr[now].val) del(tr[now].l,val);
		else del(tr[now].r,val);
	}
public:
	inline void ins(int val){ins(root,val);}
	inline void del(int val){del(root,val);}
	int getrank(int val)
	{
		int now=root,rank=1;
		while (now)
		{
			if (val<=tr[now].val) now=tr[now].l;
			else {rank+=tr[now].exist+tr[tr[now].l].fact; now=tr[now].r;}
		} return rank;
	}
	int getval(int rank)
	{
		int now=root;
		while (now)
		{
			if (tr[now].exist&&tr[tr[now].l].fact+tr[now].exist==rank) break;
			else if (tr[tr[now].l].fact>=rank) now=tr[now].l;
			else {rank-=tr[tr[now].l].fact+tr[now].exist; now=tr[now].r;}
		} return tr[now].val;
	}
	inline int pre(int x){return getval(getrank(x)-1);}
	inline int nxt(int x){return getval(getrank(x+1));}
}BST;

2(1). fhq_Treap

发明人:范浩强神犇(%%%%%%%%%%%%%%%%%%%%%%)

要学习 fhq Treap,你得先简单了解一下普通 Treap 是咋个回事。

普通 Treap 其实是一棵特殊的笛卡尔树。

Treap=Tree+Heap,这是一棵拥有双重性质的树形数据结构,既满足 BST(二叉搜索树)的性质也满足 Heap(堆)的性质。

它让平衡树上的每一个结点存放两个信息:值和一个随机的索引 key。其中值满足二叉搜索树的性质,索引满足堆的性质,结合二叉搜索树和二叉堆的性质来使树平衡。这也是 Treap 的性质。

Treap 为什么可以平衡?我们知道如果对一颗二叉搜索树进行插入的值按次序是有序的,那么二叉搜索树就会退化成一个链表。那么我们可以别让数值按次序插入,一个很好的方法就是把插入次序随机化。比如本来的插入次序是 1,2,3,4,5,6,结果随机之后变成了 3,5,2,6,1,4,是不是就好多了!

Treap 用二叉堆来维护随机索引 key,其实就是相当于把插入次序随机化。插入一值数后你必然要让 key 满足二叉堆的特性,但又因为索引是随机的,那就会导致插入的数不知道搞到了哪里去,相当于插入次序随机了。

当然你也可以选择不理解那么多,你其实只需要知道:

普通 Treap 维护平衡的操作是旋转(rorate),而 fhq_Treap 用来维护平衡的操作只有分裂(split)和合并(merge)。

fhq Treap 要存 左右子树编号,值,索引(key),子树大小

!.1 分裂

分裂有两种:按值分裂和按大小分裂

写普通平衡树一般用按值分裂,而在写文艺平衡树的时候一般用按大小分裂(后面有文艺平衡树)

按值分裂:把树拆成两棵树,拆出来的一棵树的值全部小于等于给定的值,另外一部分的值全部大于给定的值。

rV62LR.png
(一个示例)

那么如何拆呢?这就需要利用 fhq_Treap 的性质及本质了。

从一个根节点开始遍历就可以推出整棵树,所以一个 fhq_Treap 的根节点代表的是这棵 fhq_Treap 所有的点,你可以将其理解为这个 fhq_Treap 所代表的是一个 val 的区间,中序遍历后的 val 序列是单调不减的。

那么比如说一个 val 序列为 (l,r) ,你想要将 val 小于等于 queryval 的所有节点和大于 queryval 的所有节点分开,就相当于将 val 的区间 $(l,r) 拆成两个区间 (l,queryval](queryval+1,r) ,就等同于将一棵大 fhq_Treap 按照 val 分成两棵小 fhq_Treap .

我们将 (l,queryval) 称为「左区间树」,将 (queryval+1,r) 称为「右区间树」。

queryval=5 时,分裂的过程如图,图中的蓝色是已经归入左区间树的点,橙色是已经归入右区间树的点,左右区间树中的实圆是已经确定的节点,虚圆是尚未确定的节点。

我们首先建立两个虚拟节点,就是图中的虚圆,如果当前访问的节点 nowvalqueryval ,那么 nownow 的左子树,都应归在左区间树里,就是将左区间树上一个虚拟节点赋为 now,此节点便由虚变实,然后对 now 建立虚拟的右儿子。否则, nownow 的右子树应该归在右区间树里,就是将上一个右区间树的虚拟节点赋为 now,此节点便由虚变实,然后对 now 建立虚拟的左儿子。如果 now 是空节点,则将左右区间树的虚拟节点赋为 0 .

虚拟节点可以用引用存。

因为每访问到一个非空的 now 节点时,递归分裂子树进行的操作会改变左右儿子,所以在维护一些值(如子树大小)的时候需要用同线段树类似的方法进行上传操作。

Code:

inline void update(int now){fhq[now].size=fhq[fhq[now].l].size+fhq[fhq[now].r].size+1;}
void split(int now,int val,int& x,int& y)
{
    if (!now) x=y=0;
    else 
    {
        if (fhq[now].val<=val){x=now; split(fhq[now].r,val,fhq[now].r,y);}
        else{y=now; split(fhq[now].l,val,x,fhq[now].l);}
	update(now);
    }
}

!.2 合并

合并就是反向分裂。

Code:

int merge(int x,int y)
{
    if (!x||!y) return x+y;
    if (fhq[x].key>fhq[y].key){fhq[x].r=merge(fhq[x].r,y); update(x); return x;}
    else{fhq[y].l=merge(x,fhq[y].l); update(y); return y;}
    return 0; // 为了不报 Warning 写的
}

2(1).1 插入

设插入的值为 val,那么我们只需要按 val 分裂再把节点插在中间(合并)即可。

#define def1 static int x,y;
#define def2 static int x,y,z;
inline void ins(int val){def1; split(root,val,x,y); root=merge(merge(x,newnode(val)),y);}

2(1).2 删除

设删除的值是 val,那么先按 val 分裂,再按 val1 分裂,此时就分出了一棵全是 val 元素的子树,然后把那个子树的根删掉(合并左右子树),最后把分裂开的合并回去即可

inline void del(int val)
{
	def2;
	split(root,val,x,z); split(x,val-1,x,y);
	y=merge(fhq[y].l,fhq[y].r); root=merge(merge(x,y),z);
}

2(1).3 & 2(1).4 查询值的排名 / 查询排名的值

查询值的排名就先按 val1 分裂,然后看一下子树大小即可;查询排名的值按替罪羊树的写即可。

inline int getrank(int val){def1; split(root,val-1,x,y); int ans=fhq[x].size+1; root=merge(x,y); return ans;}
inline int getval(int rank)
{
	int now=root;
	while (now)
	{
		if (fhq[fhq[now].l].size+1==rank) break;
		else if (fhq[fhq[now].l].size>=rank) now=fhq[now].l;
		else{rank-=fhq[fhq[now].l].size+1; now=fhq[now].r;}
	} return fhq[now].val;
}

2(1).5 & 2(1).6 求前趋 / 求后继

前驱:按值 val1 分裂,则左子树里面最右边的数的前驱;
后继:按值 val 分裂,则右子树里面最左的数就是后继。

inline int pre(int val)
{
	def1;
	split(root,val-1,x,y); int now=x;
	while (fhq[now].r) now=fhq[now].r;
	int ans=fhq[now].val; root=merge(x,y); return ans;
}
inline int nxt(int val)
{
	def1;
	split(root,val,x,y); int now=y;
	while (fhq[now].l) now=fhq[now].l;
	int ans=fhq[now].val; root=merge(x,y); return ans;
}

完整代码

#include<random>
#include<iostream>
#include<ctime>
#include<queue>
#include<stack>
#include<cmath>
#include<iterator>
#include<cctype>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<bitset>
using namespace std;
const int N=1e5+15;
static mt19937 rnd(19260817); // C++11
struct fhq_treap
{
private:
	struct Node{int l,r,val,size,key;}fhq[N];
	int cnt,root;
	inline int newnode(int val){fhq[++cnt].val=val; fhq[cnt].key=rnd(); fhq[cnt].size=1; return cnt;}
	inline void update(int now){fhq[now].size=fhq[fhq[now].l].size+fhq[fhq[now].r].size+1;}
	void split(int now,int val,int& x,int& y)
	{
	    if (!now) x=y=0;
	    else 
	    {
	        if (fhq[now].val<=val){x=now; split(fhq[now].r,val,fhq[now].r,y);}
	        else{y=now; split(fhq[now].l,val,x,fhq[now].l);}
			update(now);
	    }
	}
	int merge(int x,int y)
	{
	    if (!x||!y) return x+y;
	    if (fhq[x].key>fhq[y].key){fhq[x].r=merge(fhq[x].r,y); update(x); return x;}
	    else{fhq[y].l=merge(x,fhq[y].l); update(y); return y;}
	    return 0;
	}
#define def1 static int x,y;
#define def2 static int x,y,z;
public:
	inline void ins(int val){def1; split(root,val,x,y); root=merge(merge(x,newnode(val)),y);}
	inline void del(int val)
	{
		def2;
		split(root,val,x,z); split(x,val-1,x,y);
		y=merge(fhq[y].l,fhq[y].r); root=merge(merge(x,y),z);
	}
	inline int getrank(int val){def1; split(root,val-1,x,y); int ans=fhq[x].size+1; root=merge(x,y); return ans;}
	inline int getval(int rank)
	{
	    int now=root;
	    while (now)
	    {
	        if (fhq[fhq[now].l].size+1==rank) break;
	        else if (fhq[fhq[now].l].size>=rank) now=fhq[now].l;
	        else{rank-=fhq[fhq[now].l].size+1; now=fhq[now].r;}
	    } return fhq[now].val;
	}
	inline int pre(int val)
	{
		def1;
	    split(root,val-1,x,y); int now=x;
	    while (fhq[now].r) now=fhq[now].r;
	    int ans=fhq[now].val; root=merge(x,y); return ans;
	}
	inline int nxt(int val)
	{
		def1;
	    split(root,val,x,y); int now=y;
	    while (fhq[now].l) now=fhq[now].l;
	    int ans=fhq[now].val; root=merge(x,y); return ans;
	}
#undef def1
#undef def2
}BST;
int main()
{
	int T; scanf("%d",&T); int opt,val;
	while (T--)
	{
		scanf("%d%d",&opt,&val);
		if (opt==1) BST.ins(val);
		else if (opt==2) BST.del(val);
		else if (opt==3) printf("%d\n",BST.getrank(val));
		else if (opt==4) printf("%d\n",BST.getval(val));
		else if (opt==5) printf("%d\n",BST.pre(val));
		else if (opt==6) printf("%d\n",BST.nxt(val));
//		else puts("ERROR");
	} return 0;
}

2(2). Treap

Treap 维护平衡的操作是旋转,在插入的时候维护即可。

因为这是 Treap,所以别的操作按 BST 写即可。

!.1 旋转

注意删除的时候要旋到下面再删,一定要上传。

旋转的具体写法可以参考后面 AVL 树部分。

fhq_Treap 的一些 Trick

1. 随机合并

在普通 Treap 中只有插入的时候用到了索引 key 的比较,在 fhq_Treap 中只有合并时用力索引比较。

既然索引只是用于随机合并一下,不如在 if 语句里直接写玄学随机罢了。

写个 rand()&1 可能会被卡,具体可以看 洛谷讨论 123169

2. 垃圾回收

将删除掉的无用的节点在下次新建节点的时候优先使用。具体实现方法为在删除节点的时候将删除的节点一个个放入队列里,在新建节点的时候判断队列是否为空,如果不为空就从队头取一个节点清空原有的所有信息并新建,否则就按正常情况新建节点。此种操作可以节省空间。

3. O(n) 建树

具体可以参考 笛卡尔树 - OI Wiki

4. 定期重构

当有些题目中对空间有一定限制,且不要求查询历史版本,我们可以与定期重构,当所用空间超过一个给定值的时候,我们就中序遍历整棵树,存入一个数组里,再线性建树。

5. 启发式合并

对于两个有交集的 fhq_Treap,很暴力的方式就是选择其中的一棵 Treap,后序遍历整棵树,将每个点左右儿子清空,拆完后作为单点与另一棵树暴力插入。然而这样的时间复杂度是不优秀的,我们考虑每次将较小的树合并到较大的树上,这样每个点最多只会合并 log 次。

3. AVL 树(平衡树始祖)

AVL 和 Treap 都是依赖旋转来维护平衡的,我们称这种平衡树为有旋平衡树,反之则成为无旋平衡树。

有旋平衡树要比无旋平衡树相对快一些(最快的平衡树好像是 RBT(Red and Blue Red Black Tree,红黑树)?)

有旋平衡树当然要先说旋转了 qwq

!.1 旋转

先放一张维基上的动图(可能动的有亿点点慢,请耐心等待)

可以发现旋转过后中序遍历不变,所以二叉搜索树不会被破坏。

Code:

inline void lrotate(int& now)
{
    int r=avl[now].r; avl[now].r=avl[r].l; avl[r].l=now; now=r;
    update(avl[now].l); update(now); // 更新信息,可以不管
}
inline void rrotate(int& now)
{
    int l=avl[now].l; avl[now].l=avl[l].r; avl[l].r=now; now=l;
    update(avl[now].r); update(now); // 更新信息,可以不管
}

?. 正题

好,有了旋转就可以开始正题了。

AVL 树是由 G.M. Adelson-Velsky 和 Evgenii Landis 于 1962 年发明的,也因此得名。而且,它是最早被发明的平衡树!

然而随着时代的进步,AVL 树已经成了时代的眼泪。

影响二叉搜索树操作最坏时间复杂度的因素是什么?

是树高,所以 AVL 保证任一结点的左右子树的最大高度差为 1,所以也叫 高度平衡树。

于此同时也保证了具有 n 个结点的 AVL 树高度一定是 O(logn),也就是最坏时间复杂度为 O(logn)

AVL 树借 平衡因子 来维持树的平衡
一个结点的平衡因子 BF = 左子树高度 - 右子树高度
根据「任一结点的左右子树的最大高度差为 1」可知:弟弟树所有结点的 BF 都只能等于 1,0,1,如果 BF 不等于这三个数,那证明不平衡,需要调整。

调整当然是按替罪羊树写啦

AVL 树用 旋转 来调整不平衡的结点,不平衡分为四种情况,每种情况都有不同的旋转方法
这四种情况用 LR 字母的组合来表示:

  • LL 的意思是:当前结点的左子树太高了,且左子树的左子树比较高;
  • LR 的意思是:当前结点的左子树太高了,且左子树的右子树比较高;
  • RL 的意思是:当前结点的右子树太高了,且右子树的左子树比较高;
  • RR 的意思是:当前结点的右子树太高了,且右子树的右子树比较高。

左子树太高了指 BF>1,比较高指 BF>0;右子树太高了指 BF<1,比较高指 BF<0 .

处理方法:

  • LL:右旋
  • RR:左旋
  • RL:左旋左子树再右旋
  • LR:右旋右子树再左旋

Code:

inline void check(int& now)
{
    int nf=BF(now);
    if (nf>1)
    {
        int lf=BF(avl[now].l);
        if (lf>0) rrotate(now);
        else lrotate(avl[now].l),rrotate(now);
    } else if (nf<-1)
    {
        int rf=BF(avl[now].r);
        if(rf<0) lrotate(now);
        else rrotate(avl[now].r),lrotate(now);
    } else if (now) update(now);
}

3.1 & 3.2 & 3.3 & 3.4 & 3.5 & 3.6 插入 / 删除 / 查询值的排名 / 查询排名的值 / 求前驱 / 求后继

按普通 BST 写即可。

完整代码

#include<iostream>
#include<ctime>
#include<queue>
#include<stack>
#include<cmath>
#include<iterator>
#include<cctype>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<bitset>
using namespace std;
const int N=1e5+15;
struct AVLTree
{
private:
	struct Node{int l,r,val,height,size;}avl[N];
	int cnt,root;
	inline void newnode(int &now,int val){avl[now=++cnt].val=val; avl[cnt].size=1;}
	inline void update(int now)
	{
	    avl[now].size=avl[avl[now].l].size+avl[avl[now].r].size+1;
		avl[now].height=std::max(avl[avl[now].l].height,avl[avl[now].r].height)+1;
	}
	inline int BF(int now){return avl[avl[now].l].height-avl[avl[now].r].height;}
	inline void lrotate(int& now)
	{
	    int r=avl[now].r; avl[now].r=avl[r].l; avl[r].l=now; now=r;
	    update(avl[now].l); update(now);
	}
	inline void rrotate(int& now)
	{
	    int l=avl[now].l; avl[now].l=avl[l].r; avl[l].r=now; now=l;
	    update(avl[now].r); update(now);
	}
	inline void check(int& now)
	{
	    int nf=BF(now);
	    if (nf>1)
	    {
	        int lf=BF(avl[now].l);
	        if (lf>0) rrotate(now);
	        else lrotate(avl[now].l),rrotate(now);
	    } else if (nf<-1)
	    {
	        int rf=BF(avl[now].r);
	        if(rf<0) lrotate(now);
	        else rrotate(avl[now].r),lrotate(now);
	    } else if (now) update(now);
	}
	void ins(int& now,int val)
	{
	    if (!now) newnode(now,val);
	    else if(val<avl[now].val) ins(avl[now].l,val);
	    else ins(avl[now].r,val);
	    check(now);
	}
	int find(int& now,int fa)
	{
	    int ret;
	    if (!avl[now].l){ret=now; avl[fa].l=avl[now].r;}
	    else{ret=find(avl[now].l,now); check(now);}
	    return ret;
	}
	void del(int& now,int val)
	{
	    if (val==avl[now].val)
	    {
	        int l=avl[now].l,r=avl[now].r;
	        if (!l||!r) now=l+r;
	        else 
	        {
	            now=find(r,r);
	            if (now!=r) avl[now].r=r;
	            avl[now].l=l;
	        }
	    }
	    else if (val<avl[now].val) del(avl[now].l,val);
	    else del(avl[now].r,val);
	    check(now);
	}
public:
	inline void ins(int val){ins(root,val);}
	inline void del(int val){del(root,val);}
	int getrank(int val)
	{
	    int now=root,rank=1;
	    while (now)
	    {
	        if (val<=avl[now].val) now=avl[now].l;
	        else{rank+=avl[avl[now].l].size+1; now=avl[now].r;}
	    } return rank;
	}
	int getval(int rank)
	{
	    int now=root;
	    while (now)
	    {
	        if (avl[avl[now].l].size+1==rank) break;
	        else if (avl[avl[now].l].size>=rank) now=avl[now].l;
	        else{rank-=avl[avl[now].l].size+1; now=avl[now].r;}
	    } return avl[now].val;
	}
	inline int pre(int x){return getval(getrank(x)-1);}
	inline int nxt(int x){return getval(getrank(x+1));}
}BST;
int main()
{
	int T; scanf("%d",&T); int opt,val;
	while (T--)
	{
		scanf("%d%d",&opt,&val);
		if (opt==1) BST.ins(val);
		else if (opt==2) BST.del(val);
		else if (opt==3) printf("%d\n",BST.getrank(val));
		else if (opt==4) printf("%d\n",BST.getval(val));
		else if (opt==5) printf("%d\n",BST.pre(val));
		else if (opt==6) printf("%d\n",BST.nxt(val));
//		else puts("ERROR");
	} return 0;
}

4. Zig-Zag Splay(Splay 的复杂代码版)

Splay 是一种自组织的数据结构,也就是说我们在使用这个数据结构的时候,我们可以通过一些情况来调整这个数据结构。例如使用频率。

做法就是对于查找频率较高的节点,使其处于离根节点相对较近的节点,但这个玩意儿确实不好统计,但是你可以认为每次被查找的点查找频率相对较高,所以每次只需要把查询到的点搬到根上就行了。

像 Cache 一样,对于经常用的数据,越用,访问得越快。

顺带一提,发明者里有 tarjan 神仙(tarjan 太强了)

!.1 伸展(Splay)

Splay 翻译过来叫伸展树,所以核心操作肯定是伸展(Splay)操作啦。

伸展即把一个结点通过旋转调整到某个结点处(一般都是伸展到根结点)

旋转就是 AVL 树那里说的左旋和右旋,不过貌似有了更高级的名字:左旋(Zag)和右旋(Zig)

Spaly 的单旋

Spaly 对于 Splay 操作的写法是:

一步一步旋转上去。

这种旋法叫做 单旋。看起来很对,是吗?

请看一个例子:

ruZCWD.gif

Splay 的双旋

一图胜千言,Splay 有四种双旋,分别是 Zig-Zig , Zig-Zag , Zag-Zig , Zag-Zag:

伸展!

采用递归 Splay 写法,可以不用维护 father,但常数会大点 .

void Splay(int x,int& y)
{
    if (x==y) return ;
    int& l=spl[y].l,&r=spl[y].r;
    if (x==l) zig(y);
    else if(x==r) zag(y);
    else
    {
        if (spl[x].val<spl[y].val)
        {
            if (spl[x].val<spl[l].val) Splay(x,spl[l].l),zig(y),zig(y);
            else Splay(x,spl[l].r),zag(l),zig(y);
        }
        else 
        {
            if (spl[x].val>spl[r].val) Splay(x,spl[r].r),zag(y),zag(y);
            else Splay(x,spl[r].l),zig(r),zag(y);
        }
    }
}

就是按双旋写的 .

3.1 & 3.2 & 3.3 & 3.4 & 3.5 & 3.6 插入 / 删除 / 查询值的排名 / 查询排名的值 / 求前驱 / 求后继

按普通 BST 写即可,注意要随时 Splay。

完整代码

struct ZigZag_SplayTree
{
private:
	struct Node{int l,r,val,size,cnt;}spl[N];
	int cnt,root;
	inline void newnode(int& now,int& val){spl[now=++cnt].val=val; ++spl[cnt].size; ++spl[cnt].cnt;}
	inline void update(int now){spl[now].size=spl[spl[now].l].size+spl[spl[now].r].size+spl[now].cnt;}
	inline void zig(int& now)
	{
	    int l=spl[now].l; spl[now].l=spl[l].r; spl[l].r=now; now=l;
	    update(spl[now].r); update(now);
	}
	inline void zag(int& now)
	{
	    int r=spl[now].r; spl[now].r=spl[r].l; spl[r].l=now; now=r;
	    update(spl[now].l); update(now);
	}
	void Splay(int x,int& y)
	{
	    if (x==y) return ;
	    int& l=spl[y].l,&r=spl[y].r;
	    if (x==l) zig(y);
	    else if(x==r) zag(y);
	    else
	    {
	        if (spl[x].val<spl[y].val)
	        {
	            if (spl[x].val<spl[l].val) Splay(x,spl[l].l),zig(y),zig(y);
	            else Splay(x,spl[l].r),zag(l),zig(y);
	        }
	        else 
	        {
	            if (spl[x].val>spl[r].val) Splay(x,spl[r].r),zag(y),zag(y);
	            else Splay(x,spl[r].l),zig(r),zag(y);
	        }
	    }
	}
	inline void delnode(int now)
	{
	    Splay(now,root);
	    if (spl[now].cnt>1) spl[now].size--,spl[now].cnt--; 
	    else if (spl[root].r)
	    {
	        int p=spl[root].r;
	        while (spl[p].l) p=spl[p].l;
	        Splay(p,spl[root].r); spl[spl[root].r].l=spl[root].l; root=spl[root].r; update(root);
	    }
	    else root=spl[root].l;
	}
	void ins(int& now,int& val)
	{
	    if (!now) newnode(now,val),Splay(now,root);
	    else if (val<spl[now].val) ins(spl[now].l,val);
	    else if (val>spl[now].val) ins(spl[now].r,val);
	    else{++spl[now].size; ++spl[now].cnt; Splay(now,root);}
	}
	void del(int now,int& val)
	{
	    if (spl[now].val==val) delnode(now);
	    else if (val<spl[now].val) del(spl[now].l,val);
	    else del(spl[now].r,val);
	}
public:
	inline void ins(int val){ins(root,val);}
	inline void del(int val){del(root,val);}
	int getrank(int val)
	{
	    int now=root,rank=1;
	    while (now)
	    {
	        if (spl[now].val==val){rank+=spl[spl[now].l].size; Splay(now,root); break;}
	        if (val<=spl[now].val) now=spl[now].l;
	        else{rank+=spl[spl[now].l].size+spl[now].cnt; now=spl[now].r;}
	    } return rank;
	}
	int getval(int rank)
	{
	    int now=root;
	    while (now)
	    {
	        int lsize=spl[spl[now].l].size;
	        if (lsize+1<=rank&&rank<=lsize+spl[now].cnt){Splay(now,root); break ;}
	        else if (lsize>=rank) now=spl[now].l;
	        else{rank-=lsize+spl[now].cnt; now=spl[now].r;}
	    } return spl[now].val;
	}
	inline int pre(int x){return getval(getrank(x)-1);}
	inline int nxt(int x){return getval(getrank(x+1));}
}BST;

半伸展

最后简单说一下半伸展。

我们在使用输入法的时候,大多数情况并不是用了一下这个词条后这个词条瞬间就到了首位了,而是随着我们对它的使用,慢慢地往首位移动,这是怎么实现的呢?

其实是对一字形旋转进行了一些改变:

s7lIDx.png

5. Splay(Splay 的精简代码版)

6. SBT(节点大小平衡树)

7. WBLT

8. RBT & LLRBT

普通平衡树其他做法

1. vector

2. 权值线段树

3. 树状数组

4. 01Trie

5. pb_ds

6. multiset

文艺平衡树

1. fhq_Treap

2. Splay

实际时空比较

普通平衡树(用的是咕咕里的普通平衡树普通版):

平衡树 代码长度 时间 空间 来源
替罪羊树 3.15KB 352ms 1.89MB 文中
替罪羊树 3.81KB 528ms 18.07MB Misaka_Azusa 的题解
fhq Treap 2.47KB 348ms 1.50MB 文中
fhq Treap 2.49KB 390ms 1.64MB 一个小屁孩 的题解
Treap 4.65KB 250ms 10.98MB 天上一颗蛋 的题解
Treap 3.15KB 293ms 1.62MB wasa855 的题解
AVL 3.08KB 278ms 1.50MB 文中
vector 758B 467ms 1.11MB 一个小屁孩 的题解

Reference

posted @   yspm  阅读(538)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
😅​
点击右上角即可分享
微信分享提示