洛谷P3391 【模板】文艺平衡树(splay)

       Splay简介

        Splay树是一种高效率的BST(二叉搜索树),他的基本操作是把节点旋转到二叉树的根部,旋转操作能有效改善树的平衡性。这个操作附带实现了一个重要应用——高效率访问“热数据”。例如,在计算机网络中,交换机上的路由表的某些IP地址是“热”地址,需要被频繁查询。若用Splay树存储路由表,可以把这些热地址旋转到二叉树的根部,从而尽快被查询到。

        Splay树的整体效率也很高,他的平摊操作次数为O(log2 n),也就是说,在一棵有n个结点的BST上做M次Splay操作,时间复杂度为O(Mlog2 n)。

        如何设计把一个节点x旋转到根的方法?需要达到一下两个目的:

        (1)每次旋转,节点x就上升一层,从而能在有限次操作后到达根部。

        (2)旋转能改善BST的平衡性,即尽量使二叉树的层次变少,从根到叶子结点的路径变短,平均的层数和路径长度为O(log2 n)。

        Splay树保证时间复杂度的核心就是每操作一个节点,将该节点旋转到树根(局部性原理)。

        如果只考虑目的(1),那么使用Treap树的旋转法即可,每次x与它的父节点交换位置,上升一层。称这种旋转为“单旋”,单旋不会减少二叉树的层数,对改善平衡性没有帮助。Splay树主要使用“双旋”,即两次单旋,同时旋转3个结点:x,父亲f,祖父g。双旋分为两种:一字旋,之字旋,能改善平衡性。

                        左旋右旋不改变树的中序遍历

 

 (1)一字型:先旋转f和g,在旋转x

   (2)之字型:先旋转x和f,在旋转x和g。

 Splay树插入节点:先按照BST树的规则,找到插入的位置,插入之后,将该插入节点旋转到树根(保证时间复杂度)。

Splay树的删除一段区间操作(本题用的同样的思路,用的懒标记):加入要删除中序遍历的[L,R],那么我们就Splay(L-1,0),将第L-1个结点旋转到树根,然后Splay(R+1,L-1)将R+1 旋转到L-1下面,此时删除R+1节点的左子树就相当于删除了[L,R]区间。

题目描述

您需要写一种数据结构(可参考题目标题),来维护一个有序数列。

其中需要提供以下操作:翻转一个区间,例如原有序序列是 5 4 3 2 1,翻转区间是 [2,4][2,4] 的话,结果是 5 2 3 4 1。

输入格式

第一行两个正整数 𝑛,𝑚,表示序列长度与操作个数。序列中第 𝑖 项初始为 𝑖i。
接下来 𝑚 行,每行两个正整数 𝑙,𝑟,表示翻转的区间。

输出格式

输出一行 𝑛 个正整数,表示原始序列经过 𝑚次变换后的结果。

输入输出样例

输入 #1复制

5 3
1 3
1 3
1 4

输出 #1复制

4 3 2 1 5

说明/提示

【数据范围】
对于 100% 的数据,1≤𝑛,𝑚≤100000,1≤𝑙≤𝑟≤𝑛。

对于本题需要维护的信息:

        (1)size:表示子树的大小,用于中序输出下标的指示。

        (2)懒标记flag:表示这个区间是否需要反转。

代码如下:

//时刻保证中序遍历是我们当前序列的顺序(只有一开始是有序的,左儿子比当前小,右儿子比当前大)
#include<bits/stdc++.h>
using namespace std;
const int N=100010;

int n,m;
struct Node
{
    int s[2],p,v;//左右儿子,父节点,当前节点编号
    int size;//表示子树大小
    int flag;//表示有没有翻转
    
    void init(int _v,int _p)
    {
        v=_v;
        p=_p;
        size=1;
    }
}tr[N];
int root,idx;

void pushup(int x)//子结点更新父节点
{
    tr[x].size=tr[tr[x].s[0]].size+tr[tr[x].s[1]].size+1;
}

void pushdown(int x)//下传懒标记
{
    if(tr[x].flag)//如果当前子树需要翻转
    {
        swap(tr[x].s[0],tr[x].s[1]);//交换左右结点
        //将懒标记下传
        tr[tr[x].s[0]].flag^=1;
        tr[tr[x].s[1]].flag^=1;
        //清空当前节点的标记
        tr[x].flag=0;
    }
}

void rotate(int x)//左旋右旋操作
{
    int y=tr[x].p;//y表示x的父节点
    int z=tr[y].p;//z表示y的父节点
    int k=tr[y].s[1]==x;//k==0表示x是y的左儿子,k==1表示x是y的右儿子
    
    tr[z].s[tr[z].s[1]==y]=x;//如果y是z的左儿子,tr[z].s[1]==y的值为0,否则为1
    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;
    //先算y的信息,在算x的信息
    pushup(y);
    pushup(x);
}

void splay(int x,int k)
{
    while(tr[x].p!=k)
    {
        int y=tr[x].p;//y是x的父节点
        int z=tr[y].p;//z是y的父节点
        if(z!=k)
            if((tr[y].s[1]==x)^(tr[z].s[1]==y))//如果是折线关系
                rotate(x);
            else//直线关系
                rotate(y);
                
        //如果z==k的话,转1次就行了
        rotate(x);
    }
    if(!k)//如果k是0,更新根节点
        root=x;
}

void insert(int v)
{
    int u=root,p=0;//p是u的父节点
    while(u)
    {
        p=u;
        u=tr[u].s[v>tr[u].v];//判断v应该插入到u的左儿子还是右儿子
    }
    u=++idx;//新分配一个编号
    if(p)//如果有父节点,那么父节点更新儿子信息
        tr[p].s[v>tr[p].v]=u;
    tr[u].init(v,p);
    
    //用来保证splay的时间复杂度
    splay(u,0);//将这个点旋转到根节点
}

int get_k(int k)//返回中序遍历的第k个数字的下标
{
    int u=root;
    while(true)
    {
        pushdown(u);
        if(tr[tr[u].s[0]].size>=k) //第k个在左子树
            u=tr[u].s[0];
        else if(tr[tr[u].s[0]].size+1==k)//第k个数刚好就是根节点
            return u;
        else
        {
            k-=tr[tr[u].s[0]].size+1;
            u=tr[u].s[1];
        }
    }
    return -1;//没有找到
}

void output(int u)//输出树的中序遍历
{
    pushdown(u);//将懒标记下传
    if(tr[u].s[0]) //如果左子树不为空,就先遍历左子树
        output(tr[u].s[0]);
    if(tr[u].v>=1&&tr[u].v<=n)//如果当前节点不是哨兵节点,就直接输出当前节点
        printf("%d ",tr[u].v);
    if(tr[u].s[1])//如果当前节点的右子树不为空,就递归遍历右子树
        output(tr[u].s[1]);
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=0;i<=n+1;i++) insert(i);//头和尾是哨兵节点,防止越界
    while(m--)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        l=get_k(l),r=get_k(r+2);//要找到L的前驱节点与R的后继节点,(本来是要找L-1,和R+1),因为0号点是哨兵,所以多加1
        splay(l,0);//将左端点旋转到根节点
        splay(r,l);//将右端点转到左端点的下面
        tr[tr[r].s[0]].flag^=1;//将右端点的左子树翻转
    }
    output(root);//输出树的中序遍历
    
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值