洛谷3391-伸展树模板

题目描述

您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:
翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1

输入格式:

第一行为n,m。n表示初始序列有n个数,这个序列依次是(1,2,⋯n−1,n); m表示翻转操作次数。
接下来m行每行两个数 [l,r] 数据保证 1 ≤ l ≤ r ≤ n

输出格式:

输出一行n个数字,表示原始序列经过m次变换后的结果

解题思路

我们现在需要这样一种数据结构,它可以快速反转指定区间的元素,并能够快速找到第k个元素的数值。考虑到序列中的元素是1~n,所以我们用常量SZ = 1e5+10来规定范围。
我们可以在序列1~n建立一棵树,这颗树上每个节点有一个变量size表示以该节点为根的树有多少个元素。如果我们想找排名第k的元素,那么每次只需和当前节点的size比较一下,就可以决定是走左子树还是右子树了。所以我们需要维护这棵树的size,并设法解决当反转一个区间[l,r]时,如何维护size。
由于旋转操作(rotate)并不会改变每个节点左边或右边的size总数,所以并不会影响每个元素的排名,因此旋转操作是被允许的。既然旋转操作被允许,那么splaying操作也可以被允许。
综上所述,我们需要实现如下2个主函数:

getValByRank(k): 获得排名第k的元素的值
reverse(a,b): 反转区间[a,b]内的元素

为了实现上述2个函数,我们还需要一些辅助函数:

buildTree(): 建树操作
splay(x): 将节点x旋转至根
init(): 负责初始化
rotate(x): 旋转节点x和其父节点连接的边,用于辅助splay
update(x): 更新节点x的size,用于辅助rotate
spread(x): 延迟标记的更新(类似于线段树),主要用于向下更新反转操作
join(x,y) 合并两棵树x、y,并返回新树的根
split(x,y,a) 将树x按照a分为两棵子树x和y
split3(x,y,z,a,b): 将树按照[1,a),[a,b],(b,n]分为x、y、z三棵子树
join3(x,y,z): 将x、y、z三棵子树合并

如何实现反转

如果想要反转区间[a,b],那么很显然原区间被分为了三份:[1,a) , [a,b] , (b,n],我们可以通过给区间[a,b]打上反转记号rev = 1,来表示该区间需要被反转。当我们访问一个节点的size时,我们就从上而下的传递rev记号,并决定是否交换每个节点的左右子树。这样做的好处是可以避免重复反转,只需在查询时对查询路径上每个点判断一次rev即可。(类似延迟标记)
所以我们需要首先将原树[1,n]分成代表[1,a),[a,b],(b,n]的三棵子树x、y、z,并对代表区间[a,b]的树根打上延迟标记rev = 1。当我们每次需要访问节点x的size时,我们spread(x)。

代码实现

#include<cstdio>
const int SZ = 1e5+10;
struct SplayTree{
    int rt,cnt,off; //根、节点个数、偏移量
    /*
    type: 0左儿子,1右儿子,ch[x][0]为左儿子编号,ch[x][1]为x右儿子编号
    size[x]代表以x为根的子树大小,fa[x]为节点x父亲编号
    tmp为临时数组,rev为反转标记数组
    */
    int type[SZ],ch[SZ][2],size[SZ],fa[SZ];
    int tmp[SZ],rev[SZ];
    //获得排名为k的元素
    int getValByRank(int x,int k){
        while(true){
            spread(x);
            if(size[ch[x][0]]+1 == k) break;
            else if(k <= size[ch[x][0]]) x = ch[x][0];
            else{
                k -= size[ch[x][0]] + 1;
                x = ch[x][1];
            }
        }
        return x;
    }
    //反转区间[a,b]
    void reverse(int a,int b){
        int x,y;
        split3(rt,x,y,a+off,b+off);
        rev[x] ^= 1;
        join3(rt,x,y);
    }
    void buildTree(int l,int r,int pa){
        if(l > r) return;
        int mid = (l+r)/2;      //取当前中点作为该区间的根
        if(mid < pa) ch[pa][0] = mid,type[mid] = 0;
        else ch[pa][1] = mid,type[mid] = 1;
        fa[mid] = pa,size[mid] = 1;
        if(l == r) return;
        buildTree(l,mid-1,mid);
        buildTree(mid+1,r,mid);
        update(mid);
    }
    void init(int n,int offset){
        rt = (n+3)/2;   //树根,取区间中点
        off = offset;   //偏移量,设起点无穷小
        type[rt] = 2;   //0表示为左子树,1为右子树,2为树根
        size[rt] = n+2; //根节点的大小是n+2
        buildTree(1,rt-1,rt);
        buildTree(rt+1,n+2,rt);
    }
    //将节点x移到树根
    void splay(int x){
        int scnt = 0;
        tmp[scnt++] = x;
        for(int i = x;type[i] != 2;i = fa[i])
            tmp[scnt++] = fa[i];//这样就能保存x到根路径上所有节点
        for(int i = scnt -1;i >= 0;i--) spread(tmp[i]);
        //分别为zig zig-zig  zig-zag旋转
         while (type[x] != 2) {
            int y = fa[x];
            if (type[x] == type[y]) rotate(y);//此处不需要update(y),下一次rotate(x)中会update(y)
            else rotate(x);
            if (type[x] == 2) break;
            rotate(x);
        }
        update(x);
    }
    //对x和fa[x]间的边做反转
    void rotate(int x){
        int t = type[x],y = fa[x],z = ch[x][1-t];
        type[x] = type[y];  //x顶替y的位置,类型自然相同
        fa[x] = fa[y];  //把y的父节点改成x的父节点
        if(type[x] != 2) ch[fa[x]][type[x]] = x;//将x与其父节点连接
        ch[x][1-t] = y; fa[y] = x; type[y] = 1-t;//将y改成x的子树
        ch[y][t] = z; //将x原来的一棵子树交给y
        if(z) fa[z] = y, type[z] = t;//如果子树z不为空,要更新信息
        update(y);     //y的size有所改变,需要更新
        //虽然x的size也改变了,但是不用急着更新,因为此时的x可能是下一轮的y
    }
    //更新节点x的size值
    inline void update(int x){
        size[x] = size[ch[x][0]] + 1 + size[ch[x][1]];
    }
    //向下传递延迟标记
    inline void swap(int &a,int &b){
        int t = a;a = b;b = t;
    }
    inline void spread(int x){
        if(rev[x] == 0) return;
        rev[x] = 0;             //清楚交换标记
        rev[ch[x][0]] ^= 1;
        rev[ch[x][1]] ^= 1;     //向下传递交换标记
        swap(ch[x][0],ch[x][1]);//交换左右儿子
        type[ch[x][0]] = 0;     //别忘了更新儿子的类型
        type[ch[x][1]] = 1;
    }
    //将树x和y合并成新树x
    inline void join(int &x,int y){
        x = getValByRank(x,size[x]);
        splay(x);   //将节点x移至根
        ch[x][1] = y,type[y] = 1,fa[y] = x;//将y变为x的右儿子
        update(x);//节点x的size改变了
    }
    //将以x为根的树按照a分成两棵树x和y
    void split(int &x,int &y,int a){
        y = getValByRank(x,a+1);
        splay(y);
        x = ch[y][0];
        type[x] = 2;    
        ch[y][0] = 0;   //左子树分离出去
        update(y);
    }
    void split3(int &x,int &y,int &z,int a,int b){
        split(x,z,b);
        split(x,y,a-1); //这里要是a-1,
    }
    void join3(int &x,int y,int z){
        join(y,z);
        join(x,y);
    }
    int getAns(int rank){   //求本题答案
        return getValByRank(rt,rank+off)-off;
    }
}sp;//树要建立在外面,占全局变量的空间
int main(){
    int n,m,a,b;
    scanf("%d%d",&n,&m);
    sp.init(n,1);
    while(m--){
        scanf("%d%d",&a,&b);
        sp.reverse(a,b);
    }
    for(int i = 1;i <= n;i++) printf("%d ",sp.getAns(i));
    return 0;
}
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:白松林 返回首页