史上第二详细的平衡树模板讲解

文章目录

  • 前言
  • 基本变量和操作
    • _pos:返回_x的位置关系
    • update:更新_x的基本信息(主要是子节点个数)
    • rotate:将某节点翻转(zig或zag)成为其父亲节点的父亲节点
    • splay:将_x号节点翻转成为_to号节点的子节点
    • insert:插入_w这个数
    • find:找到_w这个值所在点并将其旋到根上
    • findkth:找到数列中第k大的数
    • pre:找前驱
    • bac:找后继
    • del:删除数列中_w这个数
    • getrank:返回某个数的排名
  • 技巧
  • 时间复杂度
  • 关于Splay的一些细节
    • 1.关于插入极大和极小值这个技巧
    • 2.关于rotate函数的不同写法
    • 3.关于insert函数
      • A
      • B
    • 4.关于找第k大函数
    • 5.关于删点函数
    • 6.关于找排名函数
    • 7.关于函数的返回值
    • 8.关于宏的使用
  • 例题
    • luogu_P336【模板】普通平衡树
    • luoguP1486_[NOI2004]郁闷的出纳员
    • luoguP2286_[HNOI2004]宠物收养场
    • luoguP2234_[HNOI2002]营业额统计

前言

splay的关键在于rotate函数和splay函数,其他的函数就是根据二叉平衡树的性质来写的

基本变量和操作

_pos:返回_x的位置关系

update:更新_x的基本信息(主要是子节点个数)

struct node
{
	int w;//当前节点代表的权值 
	int fa;//当前节点的父亲节点 
	int son[2];//当前节点的左儿子,右儿子标号 
	int rec;//当前点代表权值在数列中重复次数 
	int numof_son;//当前点的子节点个数 
}tree[maxn];
inline bool _pos(int _x)
{
	return ((tree[tree[_x].fa].son[1]==_x)?1:0);//1是右儿子,0是左儿子 
}
inline void update(int _x)
{
	tree[_x].numof_son=tree[tree[_x].son[0]].numof_son+tree[tree[_x].son[1]].numof_son+tree[_x].rec;
}

rotate:将某节点翻转(zig或zag)成为其父亲节点的父亲节点

史上第二详细的平衡树模板讲解_第1张图片
我们不难发现,这个操作会涉及到4个点
当前节点为x
x的父节点为y
y的父节点为r
旋转时受到影响的x的子树w
将x向上转移一层,即:将r的某儿子变为x,将x的某儿子变为y,将y的某儿子变为w
但是具体怎么移动呢?
这里的关键是寻找到这些点在旋转之后的位置关系
根据二叉排序树的性质,我们模拟一下可以知道(模拟图见上),一共只有4种情况,而这所有的情况都满足如下规律:
1.y与r的位置关系和之后x与r的位置关系相同
2.变换后的w与y的位置关系与原来w和x的位置关系相反(即w是x的leftson,之后则w是y的rightson)
3.y与x的位置关系和之后x和y的位置关系相反
4.旋转时受到影响的x的子树w与x的位置关系和x与y的位置关系相反
5.旋转后的w和y的位置关系与之前x与y的位置关系相反
在实际操作中,可以画个图来看点的位置关系

inline void rotate(int _x)
{
	int _y=tree[_x].fa;
	int _r=tree[_y].fa;
	int _pos_y=_pos(_y);
	int _pos_x=_pos(_x);
	int _change_other=tree[_x].son[_pos_x^1];
	tree[_r].son[_pos_y]=_x;
	tree[_x].fa=_r;
	tree[_x].son[_pos_x^1]=_y;
	tree[_y].fa=_x;
	tree[_y].son[_pos_x]=_change_other;
	tree[_change_other].fa=_y;
	update(_y);update(_x);
}

splay:将_x号节点翻转成为_to号节点的子节点

根据tarjan巨佬大神犇的计算,我们不能直接暴力的将_x节点往上翻转,否则这颗树不再平衡,出题人会把它卡到爆的
比较优秀的做法是:
当目标和当前位置距离一层时,直接翻转_x就好
当_x和其父亲在同一条线上时(三点一线,grandfather,father,son),先旋父亲再旋自己
否则将_x向上旋转2次
(本函数里的第二个参数,当不传的时候默认为0 )

inline void splay(int _x,int _to=0)
{
	while(tree[_x].fa!=_to)
	{
		int _y=tree[_x].fa,_z=tree[_y].fa;
		if(_z!=_to)
		{
			if(_pos(_x)==_pos(_y))rotate(_y);
			else rotate(_x);
		}
		rotate(_x); 
	}
	if(_to==0)root=_x;
}  

insert:插入_w这个数

根据二分查找树的性质,我们从根节点开始,若当前节点权值大于_w,左转,反之右转
当退出第一个while循环时,有两种情况:1._w曾经被插入过 2._w还没有被插入过
由于在树中我们每个数只出现一次,重复的会被标记(即每个节点保存的是也许很多数)
所有对于情况1,我们直接对其标记累加就好
对于情况2.我们就要新建一个点
注意这个函数不要打错了

inline void insert(int _w)
{
	int _cur=root,_p=0;
	while(_cur&&tree[_cur].w!=_w)
	{
		_p=_cur;
		_cur=tree[_cur].son[_w>tree[_cur].w];
	}
	if(_cur)tree[_cur].rec++;
	else 
	{
		_cur=++cnt;
		tree[_cur].w=_w;
		if(_p)tree[_p].son[_w>tree[_p].w]=_cur;
		tree[_cur].fa=_p;
		tree[cnt].numof_son=tree[cnt].rec=1;
		tree[cnt].son[0]=tree[cnt].son[1]=0;
	}
	splay(_cur);
}

find:找到_w这个值所在点并将其旋到根上

其实_w这个值不一定存在(大多数时候是存在的)
我们面临着两种情况:
当其存在时,就可以不断二分找到它然后把它旋到root
当其不存在时,考虑分析此函数的倒数第2步,此时会发现,有四种情况,且返回_w的前驱和后继的概率相等,于是此时可能返回前驱或后继
为了消除这个随机性的影响,我们就应该将找到的这个点旋到root,这给后面查找前驱或后继提供了便利

inline void find(int _w)
{
	int _cur=root;
	while(tree[_cur].w!=_w&&tree[_cur].son[_w>tree[_cur].w])_cur=tree[_cur].son[_w>tree[_cur].w];
	splay(_cur);
}

findkth:找到数列中第k大的数

从root开始
如果当前节点的左边的数字个数小于k,那么这个点一定在右边,右转
或者当左边节点小于k且当前节点的数的个数加上左边节点的数的个数大于等于k时,找到了_w的位置,返回
剩下的情况就是右转了

inline int findkth(int _k)
{
	int _cur=root;
	while(19260817)
	{
		if(_k<=tree[tree[_cur].son[0]].numof_son)_cur=tree[_cur].son[0];
		else if(_k<=tree[tree[_cur].son[0]].numof_son+tree[_cur].rec)return _cur;
		else _k-=tree[tree[_cur].son[0]].numof_son+tree[_cur].rec,_cur=tree[_cur].son[1];
	}
}

pre:找前驱

先用find函数找,调用此函数后会有三种情况:root为_w本身,root为_w的前驱,root为_w的后继
这时就用

if(tree[root].w<_w)return root;

来处理root为_w的前驱的情况
后面的代码很好的处理了另外两种情况(可自行画图理解)

bac:找后继

似 pre()

inline int pre(int _w)
{
	find(_w);
	if(tree[root].w<_w)return root;
	int _cur=tree[root].son[0];
	while(tree[_cur].son[1])_cur=tree[_cur].son[1];
	return _cur;
}
inline int bac(int _w)
{
	find(_w);
	if(tree[root].w>_w)return root;
	int _cur=tree[root].son[1];
	while(tree[_cur].son[0])_cur=tree[_cur].son[0];
	return _cur;
}

del:删除数列中_w这个数

先把_w的前驱旋到root,再把_w的后继旋到其前驱的下面,
这个时候,根据树的性质,我们要del的数所在的节点一定是其后继的左儿子且没有子树(前驱后继之间只有_w一个数)
于是我们按照实际情况进行相关操作即可
attention:我们只是在逻辑上将点删除,在内存中这个点还是存在那里的,只是与其他点失去了联系罢了,这就可能造成空间上的浪费,比较好的解决办法就是用指针

inline void del(int _w)
{
	int _front=pre(_w);int _back=bac(_w);
	splay(_front);
	splay(_back,_front);
	int _del=tree[_back].son[0];
	if(tree[_del].rec>1)tree[_del].rec--,splay(_del);
	else tree[_back].son[0]=0;
}

getrank:返回某个数的排名

由于_find()函数返回的可能是前驱或后继或是其本身,故在查找排名时应该分类讨论
若返回的是前驱,则返回当前的root的点数和root的左儿子的子树大小
若是后继或本身,那么就返回root左子树的大小

inline int getrank(int _w){
    _find(_w);
    if(tree[root].w<_w)return tree[tree[root].son[0]].siz+tree[root].cnt+1;
    else return tree[tree[root].son[0]].siz+1;
}

技巧

int main(){
	register int n;read(n);
    register int opt,x;
    in(INF);
    in(-INF);
    //技巧:先插入两个极大极小值,防止出现插入的数没有前驱和后继的情况
	//否则若一个数没有后继,find()会出麻烦 
    loop(i,1,n){
        read(opt);read(x);
        if(opt==1)in(x);
        else if(opt==2)del(x);
        else if(opt==3)printf("%d\n",getrank(x)-1);
        else if(opt==4)printf("%d\n",tree[_findKth(x+1)].w);
        else if(opt==5)printf("%d\n",tree[pre(x)].w);
        else if(opt==6)printf("%d\n",tree[bac(x)].w);
	}
    return 0;
}

时间复杂度

关于splay的复杂度分析,本人表示无能为力,引援一篇blog

关于Splay的一些细节

本人比较蒟,每次打个splay都要调试半天,特将关于splay的坑点枚举于此

1.关于插入极大和极小值这个技巧

一定要插入足够大和小的值,否则会出一些玄学错误,一定要能多大就多大
比如这个情况

2.关于rotate函数的不同写法

rotate函数理论上来说有不同的写法,但是原则就是一定要把那四个点的父子关系和相关信息维护正确
比如:在维护x的父子关系时,要确定y是x的哪个儿子,理论上可以通过:
1.原来x相对于y的位置关系
2.x的被影响的那个子节点相对于x的位置关系
两种方式来判断
这时问题来了,如果x没有子节点呢,这时x的子节点就是0(空都是0),那么这样就没有办法正确的更新父子关系,所以以后还是用前一种方法吧

3.关于insert函数

A

if(_p)tree[_p].son[_w>tree[_p].w]=_cur;

这一句特别容易被打掉!!!!!
如果不打的话,新插入的这个点的父节点就没法和这个点相连了

B

int _cur=root,_p=0;
while(_cur&&tree[_cur].w!=_w)
{
	_p=_cur;
	_cur=tree[_cur].son[_w>tree[_cur].w];
}

在寻找值为w的点时,while循环里面的判断条件是当前所在的点不为空!

这和find函数中的即将到达的点不为空需要区别

4.关于找第k大函数

while(1){
        if(tree[Lson(cur)].siz>=k)
            cur=Lson(cur);
        else if(tree[cur].cnt+tree[Lson(cur)].siz>=k){
            Splay(cur);
            return tree[cur].w;
        }
        else if(tree[cur].cnt+tree[Lson(cur)].siz<k){
        	k-=tree[cur].cnt+tree[Lson(cur)].siz;//
        	cur=Rson(cur);
		}
    }

注意第3个else if里面的两句话的顺序不可以换!
虽然很显然,但是在没有高度专注的情况下总是会出现这些神奇的情况

5.关于删点函数

记清楚步骤:先旋前驱到root,再旋后继到其前驱的下面
不能把后继旋到根上去了

6.关于找排名函数

在大部分定义里面,数m的排名为小于m的数的个数+1,不能忘记+1!

7.关于函数的返回值

有些函数返回值可以是某个点的值,也可以是某个点的编号
但是如果返回值,就不可以得到编号,若返回编号,却可以得到值
如果不做强行规定,在构建代码的时候会出现RE的风险
因此,规定如下:

  • _pos:返回_x的位置关系:0/1
  • update:void
  • rotate:void
  • splay:void
  • insert:void
  • find:void/int(int=>返回编号,不可返回值!)
  • findkth:int(返回编号)
  • pre:int(返回编号)
  • bac:int(返回编号)
  • del:void
  • getrank:int=>返回某个数的排名

8.关于宏的使用

由于结构体的特点,用结构体方式构建的Splay必然会显得比较冗杂,语句偏长容易积累bug且不容易调试,因此推荐以下宏定义:

#define f(x) tree[x].fa
#define Lson(x) tree[x].son[0]
#define Rson(x) tree[x].son[1]
#define ADX_son(x,w) tree[x].son[tree[x].w
//按大小的儿子~~

例题

luogu_P336【模板】普通平衡树

题解代码:

#include
using namespace std;
#define loop(i,start,end) for(register int i=start;i<=end;++i)
#define ll long long
const int maxn=100000+10;
const int INF=1e9+7;
template<typename T>void read(T &x){
    x=0;char r=getchar();T neg=1;
    while(r>'9'||r<'0'){if(r=='-')neg=-1;r=getchar();}
    while(r>='0'&&r<='9'){x=(x<<1)+(x<<3)+r-'0';r=getchar();}
    x*=neg;
}
struct node{
    int f;
    int siz;
    int cnt;
    int son[2];
    int w;
}tree[maxn];
int numofp=0,root=0;
inline int getpos(int x){return ((tree[tree[x].f].son[0]==x)?0:1);}
inline void update(int x){tree[x].siz=tree[tree[x].son[0]].siz+tree[tree[x].son[1]].siz+tree[x].cnt;}
inline void Ro(int x){
    int f=tree[x].f;
	int ff=tree[f].f;
    int s=tree[x].son[(getpos(x)==0)?1:0];
    int posx=getpos(x);
    int posf=getpos(f);
    tree[f].son[posx]=s;
    tree[s].f=f;
    tree[ff].son[posf]=x;
    tree[x].f=ff;
    tree[x].son[posx^1]=f;
    tree[f].f=x;//same!
    update(f),update(x);
}
inline void Splay(int x,int to=0){
    while(tree[x].f!=to){
        int f=tree[x].f,ff=tree[f].f;
        if(ff!=to){
            if(getpos(f)==getpos(x))Ro(f);
            else Ro(x);
        }
        Ro(x);
    }
    if(to==0)root=x;
}
inline void in(int w){
    int _p=0;int cur=root;
    while(cur&&tree[cur].w!=w)_p=cur,cur=tree[cur].son[w>tree[cur].w];
    
    if(cur)tree[cur].cnt++;
    else{
        cur=++numofp;
		tree[cur].cnt=1;
        tree[cur].w=w;
        tree[cur].siz=1;
        tree[cur].son[1]=tree[numofp].son[0]=0;
        tree[cur].f=_p;
        if(_p)tree[_p].son[w>tree[_p].w]=cur;
    }
    Splay(cur);
}
inline void _find(int _w){
    int cur=root;
    while(tree[cur].w!=_w&&tree[cur].son[tree[cur].w<_w])
		cur=tree[cur].son[tree[cur].w<_w];
    Splay(cur);
}
inline int getrank(int _w){
    _find(_w);
    if(tree[root].w<_w)return tree[tree[root].son[0]].siz+tree[root].cnt+1;
    else return tree[tree[root].son[0]].siz+1;
}
inline int _findKth(int _x){
    int cur=root;
    int _rank=_x;
    while(1){
        if(_rank<=tree[tree[cur].son[0]].siz)cur=tree[cur].son[0];
		else if(_rank<=tree[tree[cur].son[0]].siz+tree[cur].cnt)return cur;
		else _rank-=tree[tree[cur].son[0]].siz+tree[cur].cnt,cur=tree[cur].son[1];
    }
}
inline int pre(int _w){
    _find(_w);
    if(tree[root].w<_w)return root;
    int cur=tree[root].son[0];
    while(tree[cur].son[1])cur=tree[cur].son[1];
    return cur;
}
inline int bac(int _w){
    _find(_w);
    if(tree[root].w>_w)return root;
    int cur=tree[root].son[1];
    while(tree[cur].son[0])cur=tree[cur].son[0];
    return cur;
}
inline void del(int _w){
    int fr=pre(_w),ba=bac(_w);
    Splay(fr);Splay(ba,fr);
    int _del=tree[ba].son[0];
    if(tree[_del].cnt>1)--tree[_del].cnt,Splay(_del);
    else tree[ba].son[0]=0;
}
int main(){
	register int n;read(n);
    register int opt,x;
    in(INF);
    in(-INF);
    loop(i,1,n){
        read(opt);read(x);
        if(opt==1)in(x);
        else if(opt==2)del(x);
        else if(opt==3)printf("%d\n",getrank(x)-1);
        else if(opt==4)printf("%d\n",tree[_findKth(x+1)].w);
        else if(opt==5)printf("%d\n",tree[pre(x)].w);
        else if(opt==6)printf("%d\n",tree[bac(x)].w);
	}
    return 0;
}

luoguP1486_[NOI2004]郁闷的出纳员

题解

luoguP2286_[HNOI2004]宠物收养场

题解

luoguP2234_[HNOI2002]营业额统计

题解

你可能感兴趣的:(数据结构,考试,总结)