平衡树基础

这么久了发现平衡树还不会背所以来复习一下。

首先你得知道什么是二叉搜索树(BST)。它是一棵二叉树,且对于任意节点都满足左儿子权值小于该节点,右儿子权值大于该节点。

我们发现普通的二叉搜索树单次查询的最坏复杂度是 \(O(n)\) 的(比如下面这张盗的图):

所以我们有几种平衡树来解决这个问题,把查询的复杂度变成 \(\Theta(\log n)\)

Treap(有旋)

Treap,是Tree和Heap拼起来的一个词。所以它既满足BST的性质又满足堆的性质。具体地,它的点权满足上面的BST性质,还使得每个节点的所有子节点权值都比该节点小。

在随机情况下,BST的树高是 \(\log n\) 级别的。所以我们随机满足堆性质的点权,就可以使它比较平衡。
我们把几个核心操作分开。

节点

struct tree{
    int son[2],val,data,size,cnt;
    //son左右儿子 val权值 data堆的权值 size子树大小 cnt相同权值的节点个数
}tree[1000010];
int New(int x){//新建节点
	tree[++t].val=x;
    tree[t].data=rand();
	tree[t].size=1;tree[t].cnt=1;
	return t;
}
void pushup(int x){
	tree[x].size=tree[tree[x].son[0]].size+tree[tree[x].son[1]].size+tree[x].cnt;//计算子树大小
}

初始化

初始化的时候我们插入两个节点, \(\infty,-\infty\) ,来防止访问到不存在的地址导致RE。

void build(){
	rt=New(-1000000000);
	tree[rt].son[1]=New(1000000000);
	pushup(rt);
}

旋转

旋转是在不影响树的性质的前提下,调整节点相互关系的一个操作,分左旋和右旋。左旋一个节点就是把这个节点旋转成左儿子,然后把右儿子提上来。右旋就是反过来。盗个图。(oiwiki的图画的真好)

通过这个我们发现,旋转(以左旋为例)有如下几个操作:

  1. 将该节点的右儿子变成原先右儿子的左儿子(2的右儿子变成4)
  2. 将该节点右儿子的左儿子变成该节点(3的右儿子变成2,即旋转上来)
  3. 将原来该节点地址的位置变成右儿子

如果是右旋那就左右翻转一下就行了。于是我们可以将两个旋转的代码放到一起写:(反正我觉得不好背,理解万岁)

void rotate(int &x,int d){//0左旋1右旋
	int tmp=tree[x].son[d^1];
	tree[x].son[d^1]=tree[tmp].son[d];
	tree[tmp].son[d]=x;
	x=tmp;
	pushup(tree[x].son[d]);pushup(x);
}

插入

和普通的BST插入没什么区别,加一个旋转就行。

void ins(int &x,int val){
	if(!x){
		x=New(val);return;//没有查到节点就新建一个
	}
	if(val==tree[x].val)tree[x].cnt++;//有就cnt++
	else{
		int d=0;
		if(val>=tree[x].val)d=1;//找到是在哪一棵子树
		ins(tree[x].son[d],val);//递归插入
		if(tree[x].data<tree[tree[x].son[d]].data)rotate(x,d^1);
        //插入的那一棵子树的权值会发生变化 如果不满足堆性质就把它转过来(左儿子右旋,右儿子左旋)
	}
	pushup(x);//更新其他信息
}

删除

同样的,我们找到这个节点之后把它旋转到最底下删掉。注意旋转的时候仍然要保证堆的性质。

void del(int &x,int val){
	if(!x)return;
	if(val==tree[x].val){//找到
		if(tree[x].cnt>1){
			tree[x].cnt--;pushup(x);
			return;
		}
		if(tree[x].son[0]||tree[x].son[1]){//如果有儿子就把它转下去
			if(!tree[x].son[1]||tree[tree[x].son[0]].data>tree[tree[x].son[1]].data){
                //我们让堆权值更大的一个儿子转上来然后递归到另一边删除
				rotate(x,1);del(tree[x].son[1],val);
			}
			else rotate(x,0);del(tree[x].son[0],val);
			pushup(x);
		}
		else x=0;//没有儿子直接删掉
		return;
	}
	if(val<tree[x].val)del(tree[x].son[0],val);//查找在哪棵子树
	else del(tree[x].son[1],val);
	pushup(x);
}

查询某个数的排名(定义为比当前数小的个数+1)

这个也可以直接在二叉树上二分递归找就行。

int getrank(int x,int val){//x的子树内val的排名
	if(!x)return 0;//没有直接返回
	if(val==tree[x].val)return tree[tree[x].son[0]].size+1;//找到返回
	else if(val<tree[x].val)return getrank(tree[x].son[0],val);//查左儿子
	else return tree[tree[x].son[0]].size+tree[x].cnt+getrank(tree[x].son[1],val);
    //查右儿子 因为所有左儿子和当前节点都比右儿子小所以加上它们的大小
}
getrank(rt,x)-1;//我们一开始插入了-inf所以要减掉

查询排名为x的数

这个也是类似的。

int getval(int x,int k){
	if(!x)return 0;//直接返回
	if(k<=tree[tree[x].son[0]].size)return getval(tree[x].son[0],k);//找左儿子
	else if(k<=tree[tree[x].son[0]].size+tree[x].cnt)return tree[x].val;//找到了
	else return getval(tree[x].son[1],k-tree[tree[x].son[0]].size-tree[x].cnt);//找右儿子 注意减掉左儿子和该节点的大小
}
getval(rt,x+1);//同理 因为我们一开始插入了-inf所以要找多一名

查询前驱/后继

int pre(int val){
	int x=rt,pre;
	while(x){
		if(tree[x].val<val){//比要求的值小 找右儿子 更新答案为该节点
			pre=tree[x].val;x=tree[x].son[1];
		}
		else x=tree[x].son[0];//比要求的值大 不能更新前驱 找左儿子
	}
	return pre;
}
int nxt(int val){
	int x=rt,nxt;
	while(x){
		if(val<tree[x].val){
			nxt=tree[x].val;x=tree[x].son[0];
		}
		else x=tree[x].son[1];
	}
	return nxt;
}

于是我们就可以切掉普通平衡树了。(就只写主函数了,不想把战线拉那么长)

int main(){
	build();
	int n;scanf("%d",&n);
	while(n--){
		int od,x;scanf("%d%d",&od,&x);
		if(od==1)ins(rt,x);
		else if(od==2)del(rt,x);
		else if(od==3)printf("%d\n",getrank(rt,x)-1);
		else if(od==4)printf("%d\n",getval(rt,x+1));
		else if(od==5)printf("%d\n",pre(x));
		else if(od==6)printf("%d\n",nxt(x));
	}
	return 0;
}

Treap(无旋)

实际上有旋Treap能干的事情线段树都能干(但是常数差不少)。所以为了整点有旋Treap不能干的东西,我们有了一些其他的平衡树,比如无旋Treap。 而且码量相当小。

无旋Treap的核心操作有两个:分裂和合并。而且貌似不需要一开始插进去 \(\infty,-\infty\)

节点

struct node{
    int son[2],val,size,data;
}tree[100010];//没怎么变 但是cnt没有了 因为无旋Treap处理重复是直接新加一个节点进去
void pushup(int x){
	tree[x].size=tree[tree[x].son[0]].size+tree[tree[x].son[1]].size+1;
}
int New(int x){
	tree[++t].size=1;tree[t].val=x;
	tree[t].data=rand();
	return t;
}

分裂

分裂操作将一棵Treap分成两部分,一部分所有节点值 \(\le val\) ,另一部分 \(> val\) 。还是盗图:

其实很简单,每次判断当前节点的值和 \(val\) 的大小关系决定划入哪一棵子树。如果小于等于,将根节点扔进第一棵子树,由于左子树全部小于等于该节点所以只需要递归右子树分裂。大于同理。

void split(int now,int val,int &x,int &y){//
	if(!now){
		x=0;y=0;return;//没有就返回
	}
	if(tree[now].val<=val){
		x=now;split(tree[now].son[1],val,tree[now].son[1],y);//递归分裂右子树
	}
	else{
		y=now;split(tree[now].son[0],val,x,tree[now].son[0]);//递归分裂左子树
	}
	pushup(now);
}

合并

合并操作将两棵Treap合并起来,其中一棵的所有节点权值小于等于另一棵。

我们可以顺序从根节点开始扫描两棵树,每次往新的树上合并一个节点,使其满足堆的性质(我写的小根堆,不要问我为什么有旋写的是大根堆)。与此同时,因为一棵的所有节点权值都小于另一棵,所以我们可以轻松地调整左右儿子,使其满足BST性质。

int merge(int x,int y){
	if(!x)return y;
	if(!y)return x;
	if(tree[x].data<tree[y].data){
		tree[x].son[1]=merge(tree[x].son[1],y);
		pushup(x);//x<y y应是x的右儿子
		return x;
	}
	else{
		tree[y].son[0]=merge(x,tree[y].son[0]);
		pushup(y);//x<y x应是y的左儿子
		return y;
	}
}

有了分裂和合并这两种操作,我们就可以用它们进行平衡树的一系列操作(当然你也可以用上面提到的那一套来搞)。

插入

假如我们要插入 \(x\) ,那么我们直接按照 \(x\) 分成两半,然后新建一个节点,合并三棵树就行了。

void ins(int val){
	split(rt,val,x,y);
	rt=merge(merge(x,New(val)),y);
}

删除

同样的按照 \(x\) 分成三棵子树然后把剩下两个合并起来。

void del(int val){
	split(rt,val,x,z);
	split(x,val-1,x,y);
	y=merge(tree[y].son[0],tree[y].son[1]);
	//可能有若干个相同的值 我们只去掉一个 所以合并左右子树 只去掉根
	rt=merge(merge(x,y),z);
}

查询x的排名

直接根据 \(x-1\) 分裂两棵树,第一棵树的节点数就是比 \(x\) 小的节点数。

int getrank(int val){
	split(rt,val-1,x,y);
	int ans=tree[x].size+1;
	rt=merge(x,y);
	return ans;
}

查询排名为x的数

我懒了直接按照普通写法写的。当然你可以按照排名分裂成三棵树然后找到中间一个的值。

int getval(int x,int k){
	if(!x)return 0;
	if(k<=tree[tree[x].son[0]].size)return getval(tree[x].son[0],k);
	else if(k==tree[tree[x].son[0]].size+1)return tree[x].val;
	else return getval(tree[x].son[1],k-tree[tree[x].son[0]].size-1);
}

查询前驱/后继

仍然分裂,前驱就按 \(x-1\) 分开然后找最大,后继就按 \(x\) 分开然后找最小。

int getpre(int val){
	split(rt,val-1,x,y);
	int ans=getval(x,tree[x].size);
	merge(x,y);
	return ans;
}
int getnxt(int val){
	split(rt,val,x,y);
	int ans=getval(y,1);
	merge(x,y);
	return ans;
}

于是我们又多了一份普通平衡树的代码。

好了说了这么半天它除了有旋Treap能干的以外它能干什么?它可以维护区间信息,一会再说。

Splay

Splay也是一种平衡树,它通过将某个节点不断旋转到根节点来保证平衡。

仍然一个一个来。

节点

int rt,t;
struct node{
    int son[2],fa,val,cnt,size;
	//多存一个父亲
}tree[100010];
bool get(int x){
	return x==tree[tree[x].fa].son[1];
}//判断是哪个儿子
void clear(int x){
	tree[x].son[0]=tree[x].son[1]=tree[x].fa=tree[x].val=tree[x].size=tree[x].cnt=0;
}
void pushup(int x){
	tree[x].size=tree[tree[x].son[0]].size+tree[tree[x].son[1]].size+tree[x].cnt;
}

旋转

这个旋转和有旋Treap的旋转是一样的,类比即可。然后这里的旋转是把一个节点旋转上去不是旋转下来,所以写法有一点点小区别。(有一说一把图背住手模几遍比什么都强)

void rotate(int x){
    int y=tree[x].fa,z=tree[y].fa;
    int tmp=get(x);
    tree[y].son[tmp]=tree[x].son[tmp^1];
    tree[tree[x].son[tmp^1]].fa=y;
    tree[x].son[tmp^1]=y;
    tree[y].fa=x;tree[x].fa=z;
    if(z)tree[z].son[y==tree[z].son[1]]=x;
    pushup(y);pushup(x);
}

Splay

在Splay中,规定每访问一个节点都要把它旋转到根节点。这样对于要旋转的节点 \(x\) 就有六种情况讨论:

  1. \(x\) 的父亲是根,直接旋转 \(x\) 即可。


  1. \(x\) 的父亲和 \(x\) 的儿子类型相同(都是左/右儿子),则先转父亲后转儿子,可以保证这三个节点仍然是一条链的形态。


  1. \(x\) 的父亲和 \(x\) 的儿子类型不同,直接转就行。


实际上就只是三点共线的时候先转父亲再转儿子,别的时候转就完了。

void splay(int x){
	for(int i=tree[x].fa;i=tree[x].fa,i;rotate(x)){
		if(tree[i].fa){
			if(get(x)==get(i))rotate(i);
			else rotate(x);
		}
		rt=x;
	}
}

插入

由于Splay要维护父亲,所以递归写起来不是很方便,于是写了非递归的版本。

void ins(int val){
	if(!rt){
		tree[++t].val=val;
		tree[t].cnt++;
		rt=t;
		pushup(rt);
		return;
	}//节点为空 直接插入
	int tmp=rt,fa=0;
	while(1){
		if(tree[tmp].val==val){//找到直接插入
			tree[tmp].cnt++;
			pushup(tmp);pushup(fa);
			splay(tmp);//别忘了
			break;
		}
		fa=tmp;tmp=tree[tmp].son[tree[tmp].val<val];
		if(!tmp){//新建一个
			tree[++t].val=val;tree[t].cnt++;
			tree[t].fa=fa;
			tree[fa].son[tree[fa].val<val]=t;
			pushup(t);pushup(fa);
			splay(t);//别忘了
			break;
		}
	}
}

查询x的排名

这个其实也可以按照别的一样写。

int getrank(int val){
	int x=0,tmp=rt;
	while(1){
		if(val<tree[tmp].val)tmp=tree[tmp].son[0];
		else{
			x+=tree[tree[tmp].son[0]].size;
			if(val==tree[tmp].val)return x+1;
			x+=tree[tmp].cnt;
			tmp=tree[tmp].son[1];
		}
	}
}

查询排名为x的数

同理。

int getval(int k){
	int tmp=rt;
	while(1){
		if(tree[tmp].son[0]&&k<=tree[tree[tmp].son[0]].size)tmp=tree[tmp].son[0];
		else{
			k-=tree[tmp].cnt+tree[tree[tmp].son[0]].size;
			if(k<=0)return tree[tmp].val;
			tmp=tree[tmp].son[1];
		}
	}
}

查询前驱/后继

我们可以插入 \(x\) ,之后由于Splay操作,前驱就变成了左子树里最大的节点,后继是右子树里最小的节点。

int pre(){
	int tmp=tree[rt].son[0];
	if(!tmp)return tmp;
	while(tree[tmp].son[1])tmp=tree[tmp].son[1];
	splay(tmp);
	return tmp;
}
int nxt(){
	int tmp=tree[rt].son[1];
	if(!tmp)return tmp;
	while(tree[tmp].son[0])tmp=tree[tmp].son[0];
	splay(tmp);
	return tmp;
}
int getpre(int x){
	ins(x);
	int ans=tree[pre()].val;
	del(x);
	return ans;
}
int getnxt(int x){
	ins(x);
	int ans=tree[nxt()].val;
	del(x);
	return ans;
}

删除

首先将要删除的节点Splay到根。然后如果个数不为 \(1\) 则减掉,否则直接合并两棵子树。

如何合并?我们现在要合并两棵Splay,其中一棵所有节点权值小于另一棵。则我们可以不断将权值小的那一棵的最大值Splay到根,然后把它的右子树设置为另一棵树并更新节点信息。

void del(int val){
	getrank(val);
	if(tree[rt].cnt>1){
		tree[rt].cnt--;pushup(rt);return;
	}
	if(!tree[rt].son[0]&&!tree[rt].son[1]){
		clear(rt);rt=0;return;
	}
	if(!tree[rt].son[0]){
		int tmp=rt;
		rt=tree[rt].son[1];
		tree[rt].fa=0;
		clear(tmp);return;
	}
	if(!tree[rt].son[1]){
		int tmp=rt;rt=tree[rt].son[0];
		tree[rt].fa=0;clear(tmp);
		return;
	}
	int tmp=rt;int x=pre();
	tree[tree[tmp].son[1]].fa=x;
	tree[x].son[1]=tree[tmp].son[1];
	clear(tmp);
	pushup(rt);
}

替罪羊树

讲解到时候再说,先上个代码。

#include <cstdio>
#include <algorithm>
#include <iostream>
#define lson tree[x].son[0]
#define rson tree[x].son[1]
using namespace std;
const double alpha=0.8;
struct node{
	int val,son[2],cnt,s,size,sd;
}tree[100010];
int cnt,rt,a[100010];
void pushup(int x){
	tree[x].s=tree[lson].s+tree[rson].s+1;
	tree[x].size=tree[lson].size+tree[rson].size+tree[x].cnt;
	tree[x].sd=tree[lson].sd+tree[rson].sd+(tree[x].cnt!=0);
}
bool judge(int x){
	return tree[x].cnt&&
		(alpha*tree[x].s<=(double)max(tree[lson].s,tree[rson].s)
		||(double)tree[x].sd<=alpha*tree[x].s);
}
void flat(int x,int &num){
	if(!x)return;
	flat(lson,num);
	if(tree[x].cnt)a[num++]=x;
	flat(rson,num);
}
int build(int l,int r){
	if(l>=r)return 0;
	int mid=(l+r)>>1;
	tree[a[mid]].son[0]=build(l,mid);
	tree[a[mid]].son[1]=build(mid+1,r);
	pushup(a[mid]);
	return a[mid];
}
void found(int &x){
	int num=0;
	flat(x,num);
	x=build(0,num);
}
void ins(int &x,int val){
	if(!x){
		x=++cnt;
		if(!rt)rt=1;
		tree[x].val=val;lson=rson=0;
		tree[x].cnt=tree[x].s=tree[x].size=tree[x].sd=1;
		return;
	}
	if(tree[x].val==val)tree[x].cnt++;
	else if(tree[x].val<val)ins(rson,val);
	else ins(lson,val);
	pushup(x);
	if(judge(x))found(x);
}
void del(int &x,int val){
	if(!x)return;
	if(tree[x].val==val){
		if(tree[x].cnt)tree[x].cnt--;
	}
	else{
		if(tree[x].val<val)del(rson,val);
		else del(lson,val);
	}
	pushup(x);
	if(judge(x))found(x);
}
int getrank(int x,int val){
	if(!x)return 0;
	else if(tree[x].val==val&&tree[x].cnt)return tree[lson].size;
	else if(val<tree[x].val)return getrank(lson,val);
	else return tree[lson].size+tree[x].cnt+getrank(rson,val);
}
int getrank2(int x,int val){
	if(!x)return 1;
	else if(tree[x].val==val&&tree[x].cnt)return tree[lson].size+tree[x].cnt+1;
	else if(val<tree[x].val)return getrank2(lson,val);
	else return tree[lson].size+tree[x].cnt+getrank2(rson,val);
}
int getval(int x,int k){
	if(!x)return 0;
	else if(tree[lson].size<k&&k<=tree[lson].size+tree[x].cnt)return tree[x].val;
	else if(k<=tree[lson].size)return getval(lson,k);
	else return getval(rson,k-tree[lson].size-tree[x].cnt);
}
int pre(int val){
	return getval(rt,getrank(rt,val));
}
int nxt(int val){
	return getval(rt,getrank2(rt,val));
}
int main(){
	int n;scanf("%d",&n);
	while(n--){
		int od,x;scanf("%d%d",&od,&x);
		if(od==1)ins(rt,x);
		else if(od==2)del(rt,x);
		else if(od==3)printf("%d\n",getrank(rt,x)+1);
		else if(od==4)printf("%d\n",getval(rt,x));
		else if(od==5)printf("%d\n",pre(x));
		else printf("%d\n",nxt(x));
	}
	return 0;
}

就完了。别的到时候再补。

posted @ 2022-09-21 20:26  gtm1514  阅读(34)  评论(0编辑  收藏  举报