平衡树(fhq-treap,splay)

平衡树概述

维护树的中序遍历不变

非旋Treap(fhq-treap)

每个节点有自己的参数 \(val\) 和随机值 \(ran\),fhq-treap在维护中序遍历的基础上,维护随机值 \(ran\) 的大根堆。因为 \(ran\) 随机,treap的期望复杂度为\(O(n \log n)\)

定义

struct treap{
	int lson,rson,val,ran,cnt/*以当前节点为根的子树大小*/;
}tr[N];
int tot,root;//节点数和当前根
void update(int x){tr[x].cnt=tr[tr[x].lson].cnt+tr[tr[x].rson].cnt+1;}

merge

合并以\(m1\)\(m2\)为根的两颗子树(维护\(ran\)的大根堆)

int merge(int m1,int m2){
	if(!m1||!m2)	return m1+m2;
	if(tr[m1].ran>=tr[m2].ran){
		tr[m1].rson=merge(tr[m1].rson,m2);
		update(m1);	return m1;
	}else{
		tr[m2].lson=merge(m1,tr[m2].lson);
		update(m2);	return m2;
	}
}

split

把树断开

将小于等于\(num\)的数放进以 \(l\) 为根的子树,大于\(num\)的数放进以 \(r\) 为根的子树(维护中序遍历不变)

void split(int x,int num,int &l,int &r){
	if(x==0){	l=0;	r=0;	return ;}
	if(tr[x].val<=num){
		l=x;
		split(tr[x].rson,num,tr[x].rson,r);
	}else{
		r=x;
		split(tr[x].lson,num,l,tr[x].lson);
	}
	update(x);
	return ;
}

其它操作均在merge和split的基础上

void insert(int x){//新建节点
	tr[++tot].val=x;	tr[tot].ran=rand();
	tr[tot].cnt=1;	tr[tot].lson=0;	tr[tot].rson=0;
	int l=0,r=0;
	split(root,x-1,l,r);
	root=merge(merge(l,tot),r);
}
void delet(int x){//删除大小为x的节点
	int l=0,r=0,pos=0;
	split(root,x,l,r);	split(l,x-1,l,pos);
	root=merge(merge(l,merge(tr[pos].lson,tr[pos].rson)),r);
}
int find_rank(int num){//查询x数的排名
	int l=0,r=0;	split(root,num-1,l,r);	int ret=tr[l].cnt+1;
	root=merge(l,r);	return ret;
}
int find_num(int rank){//查询排名为x的数
	int pos=root;
	while(pos){
		if(tr[pos].cnt==1)	return tr[pos].val;
		if(tr[tr[pos].lson].cnt>=rank)	pos=tr[pos].lson;
		else{
			rank-=tr[tr[pos].lson].cnt;
			if(rank==1)	return tr[pos].val;
			else	rank--,pos=tr[pos].rson;
		}
	}
}
int find_pre(int x){//查询前驱(小于x,且最大的数)
	int l=0,r=0;	split(root,x-1,l,r);	int pos=l;
	while(tr[pos].rson)	pos=tr[pos].rson;
	root=merge(l,r);	return tr[pos].val;
}
int find_suf(int x){//查询后继(大于x,且最小的数)
	int l=0,r=0;	split(root,x,l,r);	int pos=r;
	while(tr[pos].lson)	pos=tr[pos].lson;
	root=merge(l,r);	return tr[pos].val;
}

例题

P1486 [NOI2004]郁闷的出纳员

  1. (I命令)输入\(k\),若\(k>= min\_salary\), 则插入\(k\)
  2. (A命令)将每个数加上\(k\)
  3. (S命令)将每个数减去\(k\),并删除所有\(<min\)的数
  4. (F命令)查询第\(k\)大的数

A和S命令显然不会改变平衡树中各数的相对位置,但显然不能一个一个改

记一个变化量\(delta\),每次加减直接加减\(delta\)即可。

为了维护各数的相对大小,在插入时应该插入\(val=k-delta\)

查询时,\(k+\Delta <min\_salary\)的所有\(k\)被删除(\(\Delta\)为区间内变化量),即\(k-delta_{pre}+delta_{now}<min\_salary\),即\(val<min\_salary-delta\)\(val\)会被删掉

Splay

优点:可以指定哪一个点当根

基本操作:rotate和splay

rotate

在不破坏平衡树结构(中序遍历不变的情况下),将\(x\)向上旋转一次

其中的一种情况:

\(id(x)\)表示\(x\)是其父亲的哪个儿子

inline bool id(int x){return tr[tr[x].fa].son[1]==x;}

规律如下:

  • 自己的\(id(x)\ xor\ 1\)儿子变成父亲的\(id(x)\)儿子(自己的另外一边儿子替换自己的位置)
  • 自己接上祖父
  • 原来的父亲变成自己的\(id(x)\ xor\ 1\)儿子(即之前失去的儿子)
inline void connect(int x,int y,int pos){tr[x].fa=y;tr[y].son[pos]=x;}
inline void rotate(int x){
	int f=tr[x].fa,gf=tr[f].fa;
	int id1=id(f),id2=id(x);
	connect(tr[x].son[id2^1],f,id2);
	connect(f,x,id2^1);
	connect(x,gf,id1);
	update(f);update(x);
}

splay

一条链的情况会大大影响时间复杂度

我们需要通过这个操作减小链的长度

对于三点共线的情况,连续旋转是无效的

这时我们需要先旋转它的父亲,再旋转自己

否则就连续旋转两次自己

inline void splay(int x,int to){
	while(tr[x].fa!=to){
		if(tr[tr[x].fa].fa!=to){
			if(id(x)==id(tr[x].fa))rotate(tr[x].fa);
			else rotate(x);
		}
		rotate(x);
	}
	if(to==0)rt=x;
}

\(splay(x,y)\)的含义为:让\(x\)成为\(y\)的一个儿子,当\(y=0\)时,即让\(x\)成为根

没事就splay一下是好习惯,而且splay操作还有\(update\)的效果

其他操作都在这两个的基础上,但是比fhq-treap要复杂一些

\(find(x)\)

找到\(x\)所在节点,若不存在则返回离它最近的\(>\)它或\(<\)它的节点

inline int find(int x){
	int pos=rt;
	while(tr[pos].val!=x&&tr[pos].son[tr[pos].val<x])pos=tr[pos].son[tr[pos].val<x];
	splay(pos,0);return rt;
}

\(insert(x)\)

插入一个值为\(x\)的节点

已有则直接插入,没有则找到应该插入的位置

inline void insert(int x){
	int pos=rt,fa=0;
	while(tr[pos].val!=x&&pos)fa=pos,pos=tr[pos].son[tr[pos].val<x];
	if(pos)++tr[pos].cnt;
	else{
		pos=++cnt;tr[cnt].cnt=tr[cnt].size=1;tr[cnt].val=x;tr[cnt].son[0]=tr[cnt].son[1]=0;
		connect(cnt,fa,tr[fa].val<x);
	}
	splay(pos,0);
}

\(pre(x)\)

找到\(x\)的前驱所在的节点

inline int pre(int x){
	int pos=find(x);
	if(tr[pos].val<x)return pos;
	pos=tr[pos].son[0];
	while(pos&&tr[pos].son[1])pos=tr[pos].son[1];
	splay(pos,0);
	return rt;
}

先找,如果找到的是小于自己的,那么就一定是小于自己的最大的,直接返回。

否则先变成小于自己的,再一直向右找到最大的

\(suf(x)\)

找到\(x\)的后继所在的节点

inline int suf(int x){
	int pos=find(x);
	if(tr[pos].val>x)return pos;
	pos=tr[pos].son[1];
	while(pos&&tr[pos].son[0])pos=tr[pos].son[0];
	splay(pos,0);
	return rt;
}

原理与\(pre\)类似

\(delete(x)\)

删除一个值为\(x\)的节点

inline void delet(int x){
	int u=pre(x),v=suf(x);
	splay(u,0);splay(v,u);
	int cur=tr[v].son[0];
	if(tr[cur].cnt>1){--tr[cur].cnt;splay(cur,0);}
	else{tr[cur].size=tr[cur].cnt=0;tr[v].son[0]=0;update(v);update(u);}
}

先找到比自己的前驱和后继

然后以前驱为根,把后继接到前驱的儿子上(一定是右儿子),那么自己就一定在后继和前驱中间,即后继的左儿子

如果个数\(>1\),那么直接减个数即可

否则把这一条边断掉

查询排名和排名为\(x\)的数

inline int query_by_rank(int rank){
	int pos=rt;
	while(rank){
		if(rank<=tr[tr[pos].son[0]].size)pos=tr[pos].son[0];
		else if(rank>tr[tr[pos].son[0]].size+tr[pos].cnt)rank-=tr[tr[pos].son[0]].size+tr[pos].cnt,pos=tr[pos].son[1];
		else{splay(pos,0);return tr[pos].val;}
	}
}
inline int query_rank(int val){
	int pos=pre(val);
	return tr[pos].cnt+tr[tr[pos].son[0]].size;
}

根fhq-treap类似

posted @ 2021-01-09 17:01  harryzhr  阅读(214)  评论(0编辑  收藏  举报