Splay树模板

3224: Tyvj 1728 普通平衡树

感谢博主让我看懂了Splay删除操作写法:https://blog.csdn.net/clove_unique/article/details/50630280

下面为个人用结构体优化了splay树的写法,自己以之前写过的一些Splay树,加上博客中的理解,自己写了个Splay树的模板:

#include<algorithm>
#include<string.h>
#include<stdio.h>
using namespace std;
int n,opt,x,root,carry,tot;
//ch[2]:左右儿子,v权值,sum[2]左右儿子个数,cut本身重复个数,f父亲节点
struct node
{
    int ch[2],v,sum[2],cut,f;
} A[100010];
void new_node(int &r,int far,int k)
{
    r=++tot;
    A[r].ch[0]=A[r].ch[1]=0;
    A[r].v=k;
    A[r].sum[0]=A[r].sum[1]=0;
    A[r].f=far;
    A[r].cut=1;
    return ;
}
/*
【rol】
step 1:
找出D的父亲结点(B)以及父亲的父亲(A)并记录。判断D是B的左结点还是右结点。
step 2:
我们知道要将Drotate到B的位置,二叉树的大小关系不变的话,B就要成为D的右结点了没错吧?
咦?可是D已经有右结点了,这样不就冲突了吗?怎么解决这个冲突呢?
我们知道,D原来是B的左结点,那么rotate过后B就一定没有左结点了对吧,那么正好,我们把G接到B的左结点去,
并且这样大小关系依然是不变的,就完美的解决了这个冲突。
这样我们就完成了一次rotate,如果是右儿子的话同理。step 2的具体操作:
我们已经判断了D是B的左儿子还是右儿子,设这个关系为K;将D与K关系相反的儿子的父亲记为B与K关系相同的儿子(这里即为D的右儿子的父亲记为B的左儿子);将D与K关系相反的儿子的父亲即为B(这里即为把G的父亲记为B);将B的父亲即为D;将D与K关系相反的儿子记为B(这里即为把D
的右儿子记为B);将D的父亲记为A。
最后要判断,如果A存在(即rotate到的位置不是根的话),要把A的儿子即为D。
显而易见,rotate之后所有牵涉到变化的父子关系都要改变。以上的树需要改变四对父子关系,BG DG BD AB,需要三个操作(BG BD AB)。
step 3:update一下当前点和各个父结点的各个值
*/
void rol(int r,int kind)
{
    int far=A[r].f,gra=A[far].f,son=A[r].ch[kind];
    A[r].f=gra;
    A[son].f=far;
    A[far].f=r;
    A[r].ch[kind]=far;
    A[far].ch[!kind]=son;
    A[gra].ch[A[gra].ch[1]==far]=r;
    A[far].sum[!kind]=A[r].sum[kind];
    A[r].sum[kind]=A[far].sum[0]+A[far].sum[1]+A[far].cut;
}
/*
【splay操作】
其实splay只是rotate的发展。伸展操作只是在不停的rotate,一直到达到目标状态。如果有
一个确定的目标状态,也可以传两个参。此代码直接splay到根。
splay的过程中需要分类讨论,如果是三点一线的话(x,x的父亲,x的祖父)需要先rol
x的父亲,否则需要先rol x本身(否则会形成单旋使平衡树失衡)
*/
void splay(int r,int goal)
{
    while(A[r].f!=goal)
    {
        int far=A[r].f,gra=A[far].f,kind=A[gra].ch[0]==far;
        if(gra==goal) rol(r,A[far].ch[0]==r);
        else
        {
            if(A[far].ch[kind]==r)
            {
                rol(r,!kind);
                rol(r,kind);
            }
            else
            {
                rol(far,kind);
                rol(r,kind);
            }
        }
    }
    root=r;
    return ;
}
/*
【insert操作】

其实插入操作是比较简单的,和普通的二叉查找树基本一样。
step 1:如果root=0,即树为空的话,做一些特殊的处理,直接返回即可。
step 2:按照二叉查找树的方法一直向下找,其中:
如果遇到一个结点的关键字等于当前要插入的点的话,我们就等于把这个结点加了一个权值。
因为在二叉搜索树中是不可能出现两个相同的点的。并且要将当前点和它父亲结点的各项值更新一下。做一下splay。
如果已经到了最底下了,那么就可以直接插入。整个树的大小要+1,新结点的左儿子右儿子(虽然是空)
父亲还有各项值要一一对应。并且最后要做一下他父亲的update(做他自己的没有必要)。做一下splay。
*/
void Insert(int k)
{
    int r=root;
    if(A[r].v==k)
    {
        A[r].cut++;
        splay(r,0);
        return ;
    }
    while(A[r].ch[A[r].v<k])
    {
        r=A[r].ch[A[r].v<k];
        if(A[r].v==k)
        {
            A[r].cut++;
            splay(r,0);
            return ;
        }
    }
    new_node(A[r].ch[A[r].v<k],r,k);
    splay(A[r].ch[A[r].v<k],0);
    return ;
}
void see(int r)//debug
{
    printf("%d\n",A[r].v);
    if(A[r].ch[0])
        see(A[r].ch[0]);
    if(A[r].ch[1])
        see(A[r].ch[1]);
}
/*
【find操作】查询x的排名
初始化:ans=0,当前点=root
和其它二叉搜索树的操作基本一样。但是区别是:
如果x比当前结点小,即应该向左子树寻找,ans不用改变(设想一下,走到整棵树的最左端最底端排名不就是1吗)。
如果x比当前结点大,即应该向右子树寻找,ans需要加上左子树的大小以及根的大小(这里的大小指的是权值)。
不要忘记了再splay一下
*/
int Find(int k)//查询k的排名,从小到大数
{
    int ans=0,r=root;
    while(r)
    {
        if(A[r].v==k)
        {
            ans+=(A[r].sum[0]+1);
            splay(r,0);
            return ans;
        }
        if(A[r].v<k)
        {
            ans+=(A[r].sum[0]+A[r].cut);
            r=A[r].ch[1];
        }
        else
        {
            r=A[r].ch[0];
        }
    }
    return -1;
}
/*
【求x的前驱(后继),前驱(后继)定义为小于(大于)x,且最大(最小)的数】
这类问题可以转化为将x插入,求出树上的前驱(后继),再将x删除的问题。
其中insert操作上文已经提到。
【pre/next操作】
这个操作十分的简单,只需要理解一点:在我们做insert操作之后做了一遍splay。这就意味
着我们把x已经splay到根了。求x的前驱其实就是求x的左子树的最右边的一个结点,后继是求x的右子树的左边一个结点(想一想为什么?)
*/
int lcq(int k)
{
    Find(k);//Find是为了把x旋转到根
    if(!A[root].ch[0]) return -1;
    int r=A[root].ch[0];
    while(A[r].ch[1]) r=A[r].ch[1];
    splay(r,0);
    return A[r].v;
}
int lch(int k)
{
    Find(k);//Find是为了把x旋转到根
    if(!A[root].ch[1]) return -1;
    int r=A[root].ch[1];
    while(A[r].ch[0]) r=A[r].ch[0];
    splay(r,0);
    return A[r].v;
}
/*
【del操作】
删除操作是最后一个稍微有点麻烦的操作。
step 1:随便find一下x。目的是:将x旋转到根。
step 2:那么现在x就是根了。如果cnt[root]>1,即不只有一个x的话,直接-1返回。
step 3:如果root并没有孩子,就说名树上只有一个x而已,直接clear返回。
step 4:如果root只有左儿子或者右儿子,那么直接clear root,然后把唯一的儿子当作根就可以了(f赋0,root赋为唯一的儿子)
剩下的就是它有两个儿子的情况。
step 5:我们找到新根,也就是x的前驱(x左子树最大的一个点),将它旋转到根。然后将原来x的右子树接到新
根的右子树上(注意这个操作需要改变父子关系)。这实际上就把x删除了。不要忘了update新根。
*/
void del(int k)//
{
    if(Find(k)==-1) return ;//目的是把k旋转到根
    if(A[root].cut>1)
    {
        A[root].cut--;
        return ;
    }
    if(!A[root].ch[0]&&!A[root].ch[1])
    {
        root=0;
        return ;
    }
    if(A[root].ch[0]&&!A[root].ch[1])
    {
        int son=A[root].ch[0];
        A[son].f=0;
        root=son;
        return ;
    }
    if(!A[root].ch[0]&&A[root].ch[1])
    {
        int son=A[root].ch[1];
        A[son].f=0;
        root=son;
        return ;
    }
    int rt=A[root].ch[1];
    lcq(k);//目的是把k的前驱旋转到根
    A[root].ch[1]=rt;
    A[rt].f=root;
    A[root].sum[1]--;
    return ;
}
/*
【kth操作】找到排名为x的点
初始化:当前点=root
和上面的思路基本相同:
如果当前点有左子树,并且x比左子树的大小小的话,即向左子树寻找;
否则,向右子树寻找:先判断是否有右子树,然后记录右子树的大小以及当前点的大小(都为权值),用于判断是否需要继续向右子树寻找。
*/
int kth(int k)//查询从小到大第k大的元素
{
    int r=root;
    if(A[r].sum[0]+A[r].sum[1]+A[r].cut<k) return -1;

    while(!(k>A[r].sum[0]&&k<=A[r].sum[0]+A[r].cut))
    {
        if(A[r].sum[0]+A[r].cut<k)
        {
            k-=(A[r].cut+A[r].sum[0]);
            r=A[r].ch[1];
        }
        else
        {
            r=A[r].ch[0];
        }
    }
    return A[r].v;
}
int main()
{
    while(~scanf("%d",&n))
    {
        root=tot=0;
        for(int i=1; i<=n; i++)
        {
            scanf("%d%d",&opt,&x);
            if(opt==1) root==0?new_node(root,0,x):Insert(x);
            if(opt==2) del(x);
            if(opt==3) printf("%d\n",Find(x));
            if(opt==4) printf("%d\n",kth(x));
            if(opt==5)
            {
                Insert(x);
                printf("%d\n",lcq(x));
                del(x);
            }
            if(opt==6)
            {
                Insert(x);
                printf("%d\n",lch(x));
                del(x);
            }
        }
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值