Splay/伸展树(P3369 P3391 P2042)

什么是splay?

一种平衡二叉树

什么是平衡二叉树?

需要先了解什么是:

二叉搜索树——简称BST,每个节点最多有两个子节点,左子比当前节点小,右子比当前节点大。

因此对于插入和查找第k小的值,都可以从根递归着进行下去,在到达递归终点之前,不是选择这个节点左儿子就是右儿子,因此,操作的复杂度 = 树的深度。

然而,这棵树的形状会因为你插入数字的顺序和大小不同,导致层数过大。比如你插入 1 2 3 4 5 6 7按照前面所说,形成的树就是七层了(也就是最坏情况--退化成链状),而你修改一下这七个数插入的顺序,形成的树的形状都和当前这个不同。
这棵树的形状也太随缘了吧,这可怎么办?

平衡二叉树(以下简称平衡树)就是利用各种手段,在不改变中序遍历的情况下搞这个BST的形状,使得BST趋向于平衡,也就是层数变少。

中序遍历:不同于前序遍历先遍历左儿子右儿子再是自己。中序遍历是先左儿子再自己再右儿子。

通过中序遍历,我们可以按照从小到大的顺序获得这棵BST上的值。

各种平衡树,如treap,AVL,红黑树,SBT,splay等,都是在不改变BST中序遍历结果的情况下改变树的结构。

而中序遍历不变,那么这两颗BST是等价的:因为新的树仍然拥有原树的所有数据,并且中序遍历结果不变,意味着仍旧符合BST的性质。

换种好理解的方式来说明的话:我们说1234567这种方式的插入是形成的BST最差的,各种平衡树的平衡操作,本质上就是不改变插入的是1234567这七个数,而是通过对BST的摆弄,相当于做了改变插入顺序形成形状更好的树(注意我只是说相当于),比如说4213657这个顺序插入形成的树就非常nice。


Splay如何平衡?

Splay平衡的操作就叫做splay(伸展),这个操作基于一种叫rotate(旋转)的操作。

rotate(其实是很多平衡树的基础操作)

旋转分为左旋和右旋。

左旋就是把左儿子转到父亲的位置,让父亲变为自己的右儿子,并让左儿子的右儿子成为父亲的左儿子。

右旋就是把右儿子转到父亲的位置,让父亲变为自己的左儿子,并让右儿子的左儿子成为父亲的右儿子。

如图

代码

const int N = 1e5+5;
int fa[N];//父亲是哪个节点
int ch[N][2];//数组第二维0,1分别表示左右儿子

void rotate(int x){                            //x为将要旋转到父亲的节点,此函数能调用的条件是x有父亲
    int f = fa[x];                             //x的父亲(虽然快要给x当儿子了)
    int d = ch[f][0] == x? 1:0;                //判断是需要左旋还是右旋,d = 1表示右旋,d = 0表示左旋
    fa[x] = fa[f],fa[f] = x,fa[ch[x][d]] = f;  //正确维护旋转后的fa数组
    ch[x][d] = f,ch[f][d^1] = ch[x][d];        //改变父子关系,d^1等价于:1变0,0变1
    if (fa[x]) ch[fa[x]][ch[fa[x]]==f?0:1] = x;//原本f有父亲的话,现在需要连向x,第二维里面的那个式子是在判断f原先是其父亲的左儿子还是右儿子
    push_up(f),push_up(x);                     //像线段树一样push_up以正确维护当前节点的信息,注意顺序!!!要先f再x  
}

splay

splay其实就是不停的rotate。

splay需要传入两个参数x,goal,第一个就是需要splay的节点,第二个就是x需要不停向上转向上转,直到转到以goal为父节点。

(x:我往上转往上转,我一定要做goal的儿子啊啊啊啊啊)

当然具体没那么简单。

定义f为x的父亲、g为f的父亲,也就是x的爷爷。

我们现在要把x转到g,需要转两次,这里怎么转就有讲究了。

不会证明但是可以把BST搞平衡的旋转方法:

如果x,f,g三点一线,那么先旋转f,再旋转x

否则旋转两次x

反正...这么旋转就能平衡,每次插入一个数之后马上splay这个点到根节点这棵树就平衡了。啥均摊logN的咱也不懂,感兴趣的可以去看看证明。

代码

void splay(int x,int goal=0){//第二个蚕食不传入默认为0,只有fa[root] = 0,因此默认splay到根节点
    while (fa[x] != goal){
        int f = fa[x],g = fa[f];
        if (g!=goal) rotate((ch[f][0]==x)==(ch[g][0]==f)? f:x);//g==goal表示转一次父亲就是goal了,这个if就不用进了
        rotate(x);                                             //然后上面的if里面就是在判断祖孙仨儿是不是三点一线   
    }
    if (!goal) root = x;//此时修改根节点
}

以上就是基本平衡的操作,具体怎么实现插入删除查找通过下面的题来学!


>>>洛谷3369

6种操作:

①插入一个数

②删除一个数

③查x的排名

④查排名为x

⑤求x的前驱(比x小的最大的数)

⑥求x的后继(比x大的最小的数)

咱也分六部分讲吧,先给出开了那些数组和基本的函数,最难是删除,咱放最后讲。

int ch[N][2];//两个儿子
int fa[N];//父亲
int size[N];//子树大小(节点数)
int cnt[N];//该点的值出现了几次(插入了几个),题目明确说没有插入重复数字就可以不用这个数组,不过其实就算有也可以不用
int val[N];//节点的值
int sz,root;//sz是下一个新建节点的下标,root是根

void init(){
    sz = 1;
    root = 0;
}

int newnode(int v,int f){//v,f是将要新建节点的值,父亲
    ch[sz][0] = ch[sz][1] = 0;
    fa[sz] = f;
    size[sz] = cnt[sz] = 1;
    val[sz] = v;
    return sz++;//返回新节点下标,并自增1
}

void push_up(int rt){
    size[rt] = cnt[rt] + size[ch[rt][0]] + size[ch[rt][1]];
}

void rotate(int x){
    int f = fa[x];
    int d = ch[f][0]==x? 1:0;
    fa[x] = fa[f],fa[f] = x,fa[ch[x][d]] = f;
    ch[f][d^1] = ch[x][d];
    ch[x][d] = f;
    if (fa[x]) ch[fa[x]][ch[fa[x]][0]==f?0:1] = x;
    push_up(f);push_up(x);
}

void splay(int x,int goal=0){
    while (fa[x] != goal){
        int f = fa[x],g = fa[f];
        if (g!=goal) rotate((ch[f][0]==x)==(ch[g][0]==f)? f:x);
        rotate(x);
    }
    if (!goal) root = x;
}

插入

其实在讲splay的时候我们讲过了。

就找到合适的位置然后让父亲链上他,然后splay这个点到根就可以让树平衡了。

void insert(int v){
    int now = root,f=0;//f记录新建节点的父亲,一定要初始化为0
    while (now && val[now]!=v) f = now,now = ch[now][v>val[now]];
    if (now) cnt[now]++;//这个节点已经有了就不用新建,直接次数+1
    else {now = newnode(v,f);if (f)ch[f][v>val[f]] = now;}//newnode用于新建一个节点,然后一定要连到树上啊
    splay(now);//通过splay重构了一下树,并(主要是)正确更新了原本由于新插入一个节点而产生错误的祖先节点的信息
}

查x的排名

也就是一直深搜下去把左子树的size一直累积直到当前节点的值就是v,注意看那行注释

int rank(int v){
    int now = root,ans = 0;
    while (now){
        if (val[now]>v) now = ch[now][0];
        else{
            ans += size[ch[now][0]];
            if (val[now]==v) {splay(now);return ans+1;}//此处需要splay的原因是为了方便求前驱后继,否则可以不要
   
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值