平衡树【Splay树】学习小记

简介

平衡树,顾名思义,平衡的搜索二叉树。

常见的平衡树都能将树的深度保持在 lg ⁡ n \lg_n lgn 的级别内,防止退化成链。

一些平衡树可以通过旋转、分裂、合并等操作完成更加高级的、二叉搜索树做不到的操作。

二叉搜索树

在讲平衡树之前,先介绍这种二叉树。

1、性质&特点

首先,规定:对于节点 x x x x x x 左子树内任一点权值< x x x 权值< x x x 右子树内任一点权值。

这一条性质也被称为 bst ( b i n a r y   s e a r c h   t r e e ) \text{bst}(binary\ search\ tree) bst(binary search tree) 性质。

由此性质可以容易地发现对于一棵二叉搜索树,如果没有插入或删除,节点的中序遍历是固定的。

当然,是在没有重复元素的前提下。至于重复元素在下面。

2、支持操作

先规定以下变量、数组:

  • tot,rt:节点数量以及根的编号。

  • v[i]:节点 i i i 的权值。

  • fa[i]:节点 i i i 的父亲编号。

  • chi[i][2]:节点 i i i 的左/右儿子。

  • cnt[i]:节点 i i i 的权值存在数量(如数列1 1 2 3,如果v[3]=1,那么cnt[3]=2)。

  • sz[i]:节点 i i i 的子树中权值数量。形式化地写,sz[i]=sz[chi[i][0]]+sz[chi[i][1]]+cnt[i]

特别地,空节点编号为0。

插入一个元素:根据 bst \text{bst} bst 性质,从根开始,向左右两侧跳。直到空位或者已有该元素大小的节点。

查询元素 x x x 的前驱:先根据 bst \text{bst} bst 性质,找到元素 x x x 所在的节点。如果 x x x 有左子树,那么前驱是它左子树内最靠右的节点;如果 x x x 没有左子树但有父亲,那前驱就是它的深度最深的、 x x x 位于右子树内的祖先。

查询元素 x x x 的后继:类似前驱。

查询元素 x x x 的排名:

  • 如果 x x x 小于当前权值,向左子树。
  • 答案加上左子树大小,如果 x x x 等于当前权值,将答案加1并返回;否则加上当前点的 cnt \text{cnt} cnt 并向右子树。

查询排名为 k k k 的数值:

  • 如果 k k k 小于左子树大小,向左子树。
  • k k k 减去左子树大小。如果 k ≤ 0 k\le0 k0 ,返回当前权值;否则向右子树。

3、死因

不难发现,如果我一直插入 1 , 2 , 3...... n 1,2,3......n 1,2,3......n ,这棵树会因此而退化成一条链。

如果离线处理,可以考虑将插入的数排序后再递归建树:

Function : d g ( l , r )                                                                  i f    l = r    t h e n    i n s e r t ( l ) , r e t u r n . m i d = ( l + r ) / 2.                           i n s e r t ( m i d ) .                                d g ( l , m i d − 1 ) , d g ( m i d + 1 , r ) .   \text{Function}:dg(l,r)\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \\ \ \ \ if\ \ l=r\ \ then\ \ insert(l),return.\\ mid=(l+r)/2.\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \\ insert(mid). \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \\ dg(l,mid-1),dg(mid+1,r).\ Function:dg(l,r)                                                               if  l=r  then  insert(l),return.mid=(l+r)/2.                         insert(mid).                              dg(l,mid1),dg(mid+1,r). 

但是,如果强制在线的话,普通的平衡二叉树便会不得不退化成一条链了。

平衡树的故事从此开始。

Splay树

由Tarjan爷爷和另一位曾获图灵奖的计算机学家Daniel Sleator于1985年发明的平衡树。

1、工作原理

旋转。

具体来说,以一种方式在特定情况下(例如插入节点后)将一个节点旋转到根并不破坏 bst \text{bst} bst 性质。在这个过程中以保持树的“平衡”。

2、旋转

看起来,一步一步转比较合理。

图1:

x x x 的父亲是 f f f f f f 的父亲是 g f gf gf

如果我们要旋转一次 x x x ,那么会出现这种情况:

图2:

我们不可能像对顶堆那样做,因为我们要的是二叉树,而不是这个“乱七八糟”的玩意。(尽管现在它还是比较合理)

这时,我们发现,子树2与 f f f 空余的左子树完全吻合。因为由图一我们知道 x < [ 2 ] < f x<[2]<f x<[2]<f ,而 f f f 的左子树需要的条件同样是大于 x x x 且小于 f f f ,c此时如果我们把子树2“移到” f f f 的左子树上,问题就解决了,如图3。

图3:

所以,可以得出旋转的步骤(以上述图为例):

  • f f f 的左儿子设为 x x x 的右儿子,将 x x x 右儿子的父亲设为 f f f
  • x x x 的右儿子设为 f f f ,将 f f f 的父亲设为 x x x
  • g f gf gf 的右儿子设为 x x x ,将 x x x 的父亲设为 g f gf gf
  • 重构 x , f x,f x,f 的子树大小。

代码在第4节给出。

那么,什么时候应该旋转?

明显地,插入一个数后应该旋转以保持平衡。

并且,对于查询前驱后继,可以先把值为 x x x 的节点旋转到根。这样就只有一种情况了。

但是不难发现,如果一直旋转 x x x ,对于二叉搜索树的死因,依然会死(一条右链转为左链)。

所以,大师们有有了一个办法:如果 g f , f , x gf,f,x gf,f,x 三点“共线”,先旋转 f f f

“共线”就是说 x x x f f f 的左儿子并且 f f f g f gf gf 的左儿子,或者 x x x f f f 的右儿子并且 f f f g f gf gf 的右儿子。

至于这样操作的时间复杂度,详见大佬的证明

3、删除&实现技巧

问题来了,删除一个值为 x x x 的数怎么办?

4、一(亿)些代码&实现

  • 几个基本函数:

    //返回x作为它父亲的左还是右儿子
    int get(int x) { return chi[fa[x]][1]==x; }
    //清除点x
    void clear(int x) { sz[x]=v[x]=chi[x][0]=chi[x][1]=fa[x]=cnt[x]=0; }
    //重构x的子树大小
    void mt(int x) { sz[x]=sz[chi[x][0]]+sz[chi[x][1]]+cnt[x]; }
    
  • 旋转:

    //旋转一次x
    void rotate(int x) {
    	int f=fa[x],gf=fa[fa[x]],lx=get(x),lf=get(f);
    	
    	chi[f][lx]=chi[x][!lx];
    	if(chi[x][!lx]) fa[chi[x][!lx]]=f;
    	
    	chi[x][!lx]=f;
    	fa[f]=x;
    	
    	if(g) chi[g][lf]=x;
    	fa[x]=gf;
    	
    	mt(f);
    	mt(x);
    }
    //将x旋转到根
    void splay(int x) {
    	for(int f=fa[x];f=fa[x],f;rotate(x))
            //for中间的约束代表是x的父亲不是空节点
    		if(fa[f]) rotate(get(x)==get(f)?f:x);
    	rt=x;
    }
    
  • 插入:

    void ins(int x) {
        //空树
    	if(!rt) {
    		v[++tot]=x;
    		cnt[tot]++;
    		rt=tot;
    		mt(rt);
    		return;
    	}
    	int y=rt,f=0;
    	while(1) {
    		if(v[y]==x) {
                //有相同元素
    			cnt[y]++;
    			mt(y);
    			mt(f);
                //别忘了splay
    			splay(y);
    			break;
    		}
    		f=y;
    		y=chi[y][v[y]<x];
    		if(!y) {
                //空元素
    			v[++tot]=x;
    			cnt[tot]++;
    			fa[tot]=f;
    			chi[f][v[f]<x]=tot;
    			mt(tot);
    			mt(f);
                //别忘了splay
    			splay(tot);
    			break;
    		}
    	}
    }
    
  • 将权值为 x x x 的点旋到根

    void find(int x) {
    	int y=rt;
    	while(1) {
    		if(v[y]==x) {
    			splay(y);
    			return;
         	}
    		if(v[y]>x) y=chi[y][0];
    		else y=chi[y][1];
    	}
    }
    
  • 删除值为 x x x 的点

    //删除值为x的点 
    	void del(int x) {
    		find(x);
    		if(cnt[rt]>1) {
    			cnt[rt]--;
    			mt(rt);
    			return;
    		}
    		int ls=chi[rt][0],rs=chi[rt][1];
    		if(ls&&rs) {
    			int p=pren(x);
    			clear(rt);
    			rt=ls;
    			fa[ls]=fa[rs]=0;
    			splay(p);
    			chi[rt][1]=rs;
    			fa[rs]=rt;
    			mt(rt);
    		} else if(ls) {
    			clear(rt);
    			fa[ls]=0;
    			rt=ls;
    		} else if(rs) {
    			clear(rt);
    			fa[rs]=0;
    			rt=rs;
    		} else {
    			clear(rt);
    			tot=rt=0;
    		}
    	}
    
  • 查询 x x x 的排名

    int rk(int x) {
        //防止不存在x
    	ins(x);
        //此时x已经是根
    	int s=sz[chi[rt][0]]+1;
    	del(x);
    	return s;
    }
    
  • 前驱后继的事宜

    //返回pre编号 
    int pren(int x) {
    	ins(x);
    	int y=chi[rt][0];
    	if(!y) return 0;
    	while(chi[y][1]) y=chi[y][1];
    	del(x);
    	return y;
    }
    //返回nxt编号
    int nxtn(int x) {
    	ins(x);
    	int y=chi[rt][1];
    	if(!y) return 0;
    	while(chi[y][0]) y=chi[y][0];
    	del(x);
    	return y;
    }
    //返回pre值 
    int pre(int x) {
    	return v[pren(x)];
    }
    //返回nxt值
    int nxt(int x) {
    	return v[nxtn(x)];
    } 
    
  • 查排名为 x x x 的值

    //返回排名为x点的值 
    int kth(int x) {
    	int y=rt;
    	while(1) {
    		if(chi[y][0]&&x<=sz[chi[y][0]]) y=chi[y][0];
    		else {
    			x=x-sz[chi[y][0]]-cnt[y];
    			if(x<=0) {
    				splay(y);
    				return v[y];
    			}
    			y=chi[y][1];
    		}
    	}
    	return v[y];
    }
    

5、例题:[Luogu] P3369 【模板】普通平衡树

#include<cstdio>
using namespace std;
const int N=4e5+5;
int n;
struct phs {
	int v[N],cnt[N],fa[N],sz[N],chi[N][2],rt,tot;
	void init() {
		rt=tot=0;
	}
	int get(int x) {
		return chi[fa[x]][1]==x;
	}
	void clear(int x) {
		sz[x]=v[x]=chi[x][0]=chi[x][1]=fa[x]=cnt[x]=0;
	}
	void mt(int x) {
		sz[x]=sz[chi[x][0]]+sz[chi[x][1]]+cnt[x];
	}
	void rotate(int x) {
		int f=fa[x],gf=fa[fa[x]],lx=get(x),lf=get(f);
		
		chi[f][lx]=chi[x][!lx];
		if(chi[x][!lx]) fa[chi[x][!lx]]=f;
		
		chi[x][!lx]=f;
		fa[f]=x;
		
		if(gf) chi[gf][lf]=x;
		fa[x]=gf;
		
		mt(f);
		mt(x);
	}
	void splay(int x) {
		if(x==rt) return;
		for(int f=fa[x];f=fa[x],f;rotate(x)) {
			if(fa[f]) rotate(get(x)==get(f)?f:x);
		}
		rt=x;
	}
	void ins(int x) {
		if(!rt) {
			v[++tot]=x;
			cnt[tot]++;
			rt=tot;
			mt(rt);
			return;
		}
		int y=rt,f=0;
		while(1) {
			if(v[y]==x) {
				cnt[y]++;
				mt(y);
				mt(f);
				splay(y);
				break;
			}
			f=y;
			y=chi[y][v[y]<x];
			if(!y) {
				v[++tot]=x;
				cnt[tot]++;
				fa[tot]=f;
				chi[f][v[f]<x]=tot;
				mt(tot);
				mt(f);
				splay(tot);
				break;
			}
		}
	}
	void find(int x) {
		int y=rt;
		while(1) {
			if(v[y]==x) {
				splay(y);
				return;
			}
			if(v[y]>x) y=chi[y][0];
			else y=chi[y][1];
		}
	}
	int rk(int x) {
		ins(x);
		int s=sz[chi[rt][0]]+1;
		del(x);
		return s;
	}
	int pren(int x) {
		ins(x);
		int y=chi[rt][0];
		if(!y) return 0;
		while(chi[y][1]) y=chi[y][1];
		del(x);
		return y;
	}
	int nxtn(int x) {
		ins(x);
		int y=chi[rt][1];
		if(!y) return 0;
		while(chi[y][0]) y=chi[y][0];
		del(x);
		return y;
	}
	int pre(int x) {
		return v[pren(x)];
	}
	int nxt(int x) {
		return v[nxtn(x)];
	} 
	void del(int x) {
		find(x);
		if(cnt[rt]>1) {
			cnt[rt]--;
			mt(rt);
			return;
		}
		int ls=chi[rt][0],rs=chi[rt][1];
		if(ls&&rs) {
			int p=pren(x);
			clear(rt);
			rt=ls;
			fa[ls]=fa[rs]=0;
			splay(p);
			chi[rt][1]=rs;
			fa[rs]=rt;
			mt(rt);
		} else if(ls) clear(rt),fa[ls]=0,rt=ls;
		else if(rs) clear(rt),fa[rs]=0,rt=rs;
		else clear(rt),tot=rt=0;
	}
	int kth(int x) {
		int y=rt;
		while(1) {
			if(chi[y][0]&&x<=sz[chi[y][0]]) y=chi[y][0];
			else {
				x=x-sz[chi[y][0]]-cnt[y];
				if(x<=0) {
					splay(y);
					return v[y];
				}
				y=chi[y][1];
			}
		}
		return v[y];
	}
}t;
int main() {
	t.init();
	scanf("%d",&n);
	for(int i=1;i<=n;i++) {
		int f,x;
		scanf("%d%d",&f,&x);
		switch(f) {
			case 1:
				t.ins(x);
				break;
			case 2:
				t.del(x);
				break;
			case 3:
				printf("%d\n",t.rk(x));
				break;
			case 4:
				printf("%d\n",t.kth(x));
				break;
			case 5:
				printf("%d\n",t.pre(x));
				break;
			case 6:
				printf("%d\n",t.nxt(x));
				break;
		}
	}
}

6、高级操作

不难发现,上述操作通通都可以用权值线段树完成。

那这么长的代码能干什么!

当然是干一些很多数据结构干不了的事情,例如区间翻转。

[Luogu] P3391 【模板】文艺平衡树

大意就是翻转很多个区间。

问题来了:怎么在Splay中找到 [ l , r ] [l,r] [l,r]

我们让Splay的中序遍历等于原序列,树上「中序遍历」的第 k k k 个点就是序列的第 k k k 位。

接着把「中序遍历」的第 l − 1 l-1 l1 个点旋转到根,「中序遍历」的第 r + 1 r+1 r+1 个点旋转到 l − 1 l-1 l1 的右儿子(显然它不会到左儿子)。

现在的中序遍历: [ 1 ] , l − 1 , [ 2 ] , r + 1 , [ 3 ] [1],l-1,[2],r+1,[3] [1],l1,[2],r+1,[3]

显然, [ 2 ] [2] [2] 就是区间 [ l , r ] [l,r] [l,r]

然后我们要把 [ 2 ] [2] [2] 的全部左右儿子交换,这样Splay的中序遍历就是翻转后的序列。

类比线段树,我们用懒标记。

因为我们旋转的是「中序遍历」,所以丝毫不用担心翻转后影响什么。

此时的树不满足 bst \text{bst} bst 性质。

实现上,找「中序遍历」的第 k k k 个点,魔改一下查排名就可以了。

而且 l − 1 l-1 l1 可能为0, r + 1 r+1 r+1 可能为 n + 1 n+1 n+1 。所以要把这两个点建到树里,然后一切查「中序遍历」的第 k k k 个点都要变成 k + 1 k+1 k+1

建树类比线段树。

可以发现,如果要让Splay像线段树一样维护区间和的话,也是如此的操作。

本题代码:

#include<cstdio>
using namespace std;
const int N=100005;
int n,m;
void swap(int&a,int&b) {
	int t=a;a=b,b=t;
}
struct vsplay {
	int rt,tot,chi[N][2],sz[N],fa[N],v[N],lz[N];
	vsplay() {
		rt=tot=0;
	}
	void mt(int x) {
		sz[x]=sz[chi[x][0]]+sz[chi[x][1]]+1;
	}
	int get(int x) {
		return x==chi[fa[x]][1];
	}
	void check(int x) {
		if(lz[x]) {
			if(chi[x][0]) lz[chi[x][0]]^=1;
			if(chi[x][1]) lz[chi[x][1]]^=1;
			swap(chi[x][0],chi[x][1]);
			lz[x]=0;
		}
	}
	void rotate(int x) {
		int f=fa[x],g=fa[fa[x]],lx=get(x),lf=get(f);
		check(x),check(f);
		chi[f][lx]=chi[x][!lx];
		if(chi[x][!lx]) fa[chi[x][!lx]]=f;
		chi[x][!lx]=f;
		fa[f]=x;
		fa[x]=g;
		if(g) chi[g][lf]=x;
		mt(f);
		mt(x);
	}
	void splay(int x,int to) {
		fa[rt]=0;
		for(int f=fa[x];f=fa[x],f!=to;rotate(x))
			if(fa[f]!=to) rotate(get(x)==get(f)?f:x);
		if(!to) rt=x;
	}
	int biuld(int l,int r,int f) {
		if(l>r) return 0;
		int mid=l+r>>1;
		int x=++tot;
		v[x]=mid;
		fa[x]=f;
		sz[x]=1;
		chi[x][0]=biuld(l,mid-1,x);
		chi[x][1]=biuld(mid+1,r,x);
		mt(x);
		return x;
	}
	int kth(int k) {
		int y=rt;
		while(1) {
			check(y);
			if(chi[y][0]&&k<=sz[chi[y][0]]) y=chi[y][0];
			else {
				k=k-sz[chi[y][0]]-1;
				if(k<=0) return y;
				y=chi[y][1];
			}
		}
	}
	void solve(int x,int y) {
        //因为都要+1
		int l=kth(x),r=kth(y+2);
		splay(l,0);
		splay(r,rt);
		int gt=chi[chi[rt][1]][0];
		if(gt) lz[gt]^=1;
	}
	void ip(int x) {
		check(x);
		if(chi[x][0]) ip(chi[x][0]);
		if(1<=v[x]&&v[x]<=n) printf("%d ",v[x]);
		if(chi[x][1]) ip(chi[x][1]);
	}
	void print() {
		ip(rt);
		printf("\n");
	}
}t;
int main() {
	scanf("%d%d",&n,&m);
	t.rt=t.biuld(0,n+1,0);
	while(m--) {
		int l,r;
		scanf("%d%d",&l,&r);
		t.solve(l,r);
	}
	t.print();
}
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值