splay的一些操作

秦同学的详细版本

splay就是一种将树上的一个节点经过旋转到节点的一种操作,来保持树的平衡

splay本质:二叉搜索树

特点:结点x的左子树权值都小于x的权值,右子树权值都大于x的权值

  1. 如果当前处于共线状态的话,那么先旋转y,再旋转x.这样可以强行让他们不处于共线状态,然后平衡这棵树.
  2. 如果当前不是共线状态的话,那么只要旋转x即可.

普通平衡树
基本上每个平台都有的模板题。

为什么要splay?仅仅是插入不是插进去就行吗?成不成为根节点有什么关系?

这是为了查找比x小/大的第一个数做铺垫,因为有可能x在树中没有出现过,所以先插入x,再找前驱/后继,这就可以直接从根节点找起)

splay的旋转操作

void pushup(int u)
{
    tr[u].size=tr[tr[u].s[1]].size+tr[tr[u].s[0]].size+tr[u].cnt;
}

void rotate(int x)
{
    int y=tr[x].p,z=tr[y].p;
    int k=tr[y].s[1]==x;
    tr[z].s[tr[z].s[1]==y]=x,tr[x].p=z;//第一条线
    tr[y].s[k]=tr[x].s[k^1],tr[tr[x].s[k^1]].p=y;//第三条线
    tr[x].s[k^1]=y,tr[y].p=x;//第2条线
    pushup(y),pushup(x);
}

splay操作

void splay(int x,int goal)//将x旋转为goal的儿子,如果goal是0则旋转到根
{
    while(t[x].ff!=goal)//一直旋转到x成为goal的儿子
    {
        int y=t[x].ff,z=t[y].ff;//父节点祖父节点
        if(z!=goal)//如果Y不是根节点,则分为上面两类来旋转
            (t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);//判断共线还是不共线
            //这就是之前对于x和y是哪个儿子的讨论
        rotate(x);//无论怎么样最后的一个操作都是旋转x
    }
    if(goal==0)root=x;//如果goal是0,则将根节点更新为x
}

查找find操作

从根节点开始,左侧都比他小,右侧都比他大,
所以只需要相应的往左/右递归
如果当前位置的val已经是要查找的数
那么直接把他Splay到根节点,方便接下来的操作

void find(int x)//查找x的位置,并将其旋转到根节点
{
    int u=root;
    if(!u) return ;//树空
    while(tr[u].s[x>tr[u].v]&&x!=tr[u].v)//当存在儿子并且当前位置的值不等于x
    u=tr[u].s[x>tr[u].v];//跳转到儿子,查找x的父节点
    splay(u,0);//把当前位置旋转到根节点
}

Insert操作

往Splay中插入一个数
类似于Find操作,只是如果是已经存在的数,就可以直接在查找到的节点的进行计数
如果不存在,在递归的查找过程中,会找到他的父节点的位置,
然后就会发现底下没有啦。。。
所以这个时候新建一个节点就可以了

inline void insert(int x)//插入x
{
    int u=root,ff=0;//当前位置u,u的父节点ff
    while(u&&t[u].val!=x)//当u存在并且没有移动到当前的值
    {
        ff=u;//向下u的儿子,父节点变为u
        u=t[u].ch[x>t[u].val];//大于当前位置则向右找,否则向左找
    }
    if(u)//存在这个值的位置
        t[u].cnt++;//增加一个数
    else//不存在这个数字,要新建一个节点来存放
    {
        u=++tot;//新节点的位置
        if(ff)//如果父节点非根
            t[ff].ch[x>t[ff].val]=u;
        t[u].ch[0]=t[u].ch[1]=0;//不存在儿子
        t[tot].ff=ff;//父节点
        t[tot].val=x;//值
        t[tot].cnt=1;//数量
        t[tot].size=1;//大小
    }
    splay(u,0);//把当前位置移到根,保证结构的平衡
}

前驱/后继操作Next

首先就要执行Find操作
把要查找的数弄到根节点
然后,以前驱为例
先确定前驱比他小,所以在左子树上
然后他的前驱是左子树中最大的值
所以一直跳右结点,直到没有为止
找后继反过来就行了

int Next(int x,int f)//查找x的前驱(0)或者后继(1)
{
    find(x);
    int u=root;//根节点,此时x的父节点(存在的话)就是根节点
    if(t[u].val>x&&f)return u;//如果当前节点的值大于x并且要查找的是后继
    if(t[u].val<x&&!f)return u;//如果当前节点的值小于x并且要查找的是前驱
    u=t[u].ch[f];//查找后继的话在右儿子上找,前驱在左儿子上找
    while(t[u].ch[f^1])u=t[u].ch[f^1];//要反着跳转,否则会越来越大(越来越小)
    return u;//返回位置
}

删除操作

现在就很简单啦
首先找到这个数的前驱,把他Splay到根节点
然后找到这个数后继,把他旋转到前驱的底下
比前驱大的数是后继,在右子树
比后继小的且比前驱大的有且仅有当前数
在后继的左子树上面,
因此直接把当前根节点的右儿子的左儿子删掉就可以啦

void dele(int x)
{
    int last=Next(x,0);//找到前驱
    int next=Next(x,1);//找的后继
    splay(last,0),splay(next,last);
     //将前驱旋转到根节点,后继旋转到根节点下面
    //很明显,此时后继是前驱的右儿子,x是后继的左儿子,并且x是叶子节点
    int del=tr[next].s[0];//后继的左儿子
    if(tr[del].cnt>1)//如果超过一个
    {
        tr[del].cnt--;
        splay(del,0);
    }
    else tr[next].s[0]=0;//这个节点直接丢掉(不存在了)
}

查找第K大

从当前根节点开始,检查左子树大小
因为所有比当前位置小的数都在左侧
如果左侧的数的个数多余K,则证明第K大在左子树中
否则,向右子树找,找K-左子树大小-当前位置的数的个数
记住特判K恰好在当前位置

int  kth(int x)//查找排名为x的数
{
    int u=root;
    if(tr[u].size<x)//如果当前树上没有这么多数
    return 0;//不纯在
    while(true)
    {
        int y=tr[u].s[0];//左儿子
        if(x>tr[y].size+tr[u].cnt)
        {//如果排名比左儿子的大小和当前节点的数量要大
            x-=tr[y].size+tr[u].cnt;
            u=tr[u].s[1];
        }else if(tr[y].size>=x)//否则的话在当前节点或者左儿子上查找
        u=y;
        else //否则就是在当前根节点上
        return tr[u].v;
    }
}

完整代码

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
struct node
{
    int s[2];
    int p,v,cnt,size;
}tr[N*4];
int idx,n,m,root;

void pushup(int u)
{
    tr[u].size=tr[tr[u].s[1]].size+tr[tr[u].s[0]].size+tr[u].cnt;
}

void rotate(int x)
{
    int y=tr[x].p,z=tr[y].p;
    int k=tr[y].s[1]==x;
    tr[z].s[tr[z].s[1]==y]=x,tr[x].p=z;
    tr[y].s[k]=tr[x].s[k^1],tr[tr[x].s[k^1]].p=y;
    tr[x].s[k^1]=y,tr[y].p=x;
    pushup(y),pushup(x);
}

void splay(int x,int k)
{
    while(tr[x].p!=k)
    {
        int y=tr[x].p,z=tr[y].p;
        if(z!=k)
        if((tr[y].s[1]==x)^(tr[z].s[1]==y)) rotate(x);
        else rotate(y);
        rotate(x);
    }
    if(!k) root=x;
}

void find(int x)
{
    int u=root;
    if(!u) return ;
    while(tr[u].s[x>tr[u].v]&&x!=tr[u].v)
    u=tr[u].s[x>tr[u].v];
    
    splay(u,0);
}

void insert(int x)
{
    int u=root,p=0;
    while(u&&tr[u].v!=x)
    p=u,u=tr[u].s[x>tr[u].v];
    if(u) tr[u].cnt++;
    else
    {
        u=++idx;
        if(p) tr[p].s[x>tr[p].v]=u;
        tr[u].s[1]=tr[u].s[0]=0;
        tr[u].p=p;
        tr[u].size=1;
        tr[u].cnt=1;
        tr[u].v=x;
    }
    splay(u,0);
}

int  kth(int x)//查找排名为x的数
{
    int u=root;
    if(tr[u].size<x)//如果当前树上没有这么多数
    return 0;//不纯在
    while(true)
    {
        int y=tr[u].s[0];//左儿子
        if(x>tr[y].size+tr[u].cnt)
        {//如果排名比左儿子的大小和当前节点的数量要大
            x-=tr[y].size+tr[u].cnt;
            u=tr[u].s[1];
        }else if(tr[y].size>=x)//否则的话在当前节点或者左儿子上查找
        u=y;
        else //否则就是在当前根节点上
        return tr[u].v;
    }
}

int Next(int x,int f)
{
    find(x);
    int u=root;
    if(tr[u].v>x&&f) return u;
    if(tr[u].v<x&&!f) return u;
    u=tr[u].s[f];//查找后继的话在右儿子上找,前驱在左儿子上找
    while(tr[u].s[f^1])u=tr[u].s[f^1];//要反着跳转,否则会越来越大(越来越小)
    return u;
}

void dele(int x)
{
    int last=Next(x,0);//找到前驱
    int next=Next(x,1);//找的后继
    splay(last,0),splay(next,last);
     //将前驱旋转到根节点,后继旋转到根节点下面
    //很明显,此时后继是前驱的右儿子,x是后继的左儿子,并且x是叶子节点
    int del=tr[next].s[0];//后继的左儿子
    if(tr[del].cnt>1)//如果超过一个
    {
        tr[del].cnt--;
        splay(del,0);
    }
    else tr[next].s[0]=0;//这个节点直接丢掉(不存在了)
}


int main()
{
    cin>>n;
    insert(-1e9);
    insert(1e9);
    while(n--)
    {
        int op,x;
        cin>>op>>x;
        if(op==1)
        insert(x);
        else if(op==2)
        dele(x);
        else if(op==3)
        {
            find(x);
            printf("%d\n",tr[tr[root].s[0]].size);
        }else if(op==4)
        {
            printf("%d\n",kth(x+1));
        }
        else  if (op==5)
            printf("%d\n",tr[Next(x,0)].v);
        if (op==6)
            printf("%d\n",tr[Next(x,1)].v);
    }
    
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值