YZhe的头像

Splay——学习笔记

@

代码实现以及作为基础的平衡树的应用

模板题

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

插入xx数
删除xx数(若有多个相同的数,因只删除一个)
查询xx数的排名(排名定义为比当前数小的数的个数+1+1。若有多个相同的数,因输出最小的排名)
查询排名为xx的数
求xx的前驱(前驱定义为小于xx,且最大的数)
求xx的后继(后继定义为大于xx,且最小的数)

先贴个代码

代码如下

#include<cstdio>
using namespace std;
#define ri register int
#define ll long long
const int N=200007;
int n;
template<class T>inline void read(T &res){
    static char son;T flag=1;
    while((son=getchar())<'0'||son>'9') if(son=='-') flag=-1;
    res=son-48;
    while((son=getchar())>='0'&&son<='9') res=(res<<1)+(res<<3)+son-48;
    res*=flag;
}
class Splay{
    public:
        int root,tot=0,fa[N],size[N],cnt[N],val[N],son[N][2];
    private:
        void update(int p){
            size[p]=size[son[p][0]]+size[son[p][1]]+cnt[p];
        }
        int check(int p){
            return p==son[fa[p]][1];
        }
        void rotate(int p){
            int f=fa[p],ff=fa[f],k=check(p),kk=check(f),sp=son[p][k^1];
            son[p][k^1]=f;fa[f]=p;
            son[f][k]=sp;fa[sp]=f;
            son[ff][kk]=p;fa[p]=ff;
            update(f);update(p);
        }
        void splay(int p,int goal){
            if(p==goal) return;//易忘
            while(fa[p]!=goal){
                int f=fa[p],ff=fa[f];
                if(ff!=goal){
                    if(check(p)==check(f)) rotate(f);
                    else rotate(p);
                }
                rotate(p);
            }
            if(goal==0)	root=p;//易忘 
        }//把p   Splay到goal的儿子
        void find(int x){
            int cur=root;
            while(son[cur][x>val[cur]]&&x!=val[cur])
                cur=son[cur][x>val[cur]];
            splay(cur,0);
        }
    public:
        int rank(int x){
            find(x);
            return size[son[root][0]]+1;
        }
        void insert(int x){
            int cur=root,p=0;//p是cur的父亲
            while(cur&&x!=val[cur]){
                p=cur;
                cur=son[cur][x>val[cur]];
            }
            if(cur) ++cnt[cur];//找到了x 
            else{
                cur=++tot;
                son[cur][0]=son[cur][1]=0;
                fa[cur]=p;
                val[cur]=x;
                cnt[cur]=size[cur]=1;//要赋全 
                if(p) son[p][x>val[p]]=cur;//一定要判断
            }
            splay(cur,0);
        }
        int pre(int x){
            find(x);
            if(val[root]<x) return root;
            int p=son[root][0];
            while(son[p][1]) p=son[p][1];
            return p;
        }//记得返回的是位置而不是实际的值 
        int nxt(int x){
            find(x);
            if(val[root]>x) return root;
            int p=son[root][1];
            while(son[p][0]) p=son[p][0];
            return p;
        }
        void del(int x){
            int pr=pre(x),nt=nxt(x);
            splay(pr,0);splay(nt,pr);
            int p=son[nt][0];
            if(cnt[p]>1){
                --cnt[p];
                splay(p,0);
            }
            else son[nt][0]=0;
            update(nt);update(root);
        }
        int search(int rk){
            int p=root;
            while(1){
                if(son[p][0]&&rk<=size[son[p][0]]) p=son[p][0];//一定要判断是否有儿子 
                else if(rk>size[son[p][0]]+cnt[p]){
                    rk-=size[son[p][0]]+cnt[p];
                    p=son[p][1];//注意顺序
                }
                else return p;
            }
        }
}Tree;
int main()
{
    Tree.insert(2147483647);
    Tree.insert(-2147483647);//为了上下界减少讨论,我们插入两个标兵元素
    read(n);
    while(n--){
        int opt,x;
        read(opt);
        read(x);
        switch(opt){
            case 1:{
                Tree.insert(x);
                break;
            }
            case 2:{
                Tree.del(x);
                break;
            }
            case 3:{
                printf("%d\n",Tree.rank(x)-1);
                break;
            }
            case 4:{
                printf("%d\n",Tree.val[Tree.search(x+1)]);
                break;
            }
            case 5:{
                printf("%d\n",Tree.val[Tree.pre(x)]);
                break;
            }
            case 6:{
                printf("%d\n",Tree.val[Tree.nxt(x)]);
                break;
            }
        }
    }
    return 0;
}

在维护序列中的应用(重点)

就像线段树既可以维护一个序列,在序列中完成各种灵活区间操作,也可以作为值域线段树,来维护具体的一些数值;
\(splay\)同样拥有维护一个序列的功能。

此时,\(splay\)中的排名不在是具体的数的排名,而是对应了一个数列的相应的位置,它具备很多优良的性质,我会列举几个常用的功能。

首先,我们要明确一件事,那就是我们维护的区间在什么地方。

根据平衡树的性质易得 我们的区间就保存在splay的中序遍历中(仔细思考),因为旋转不影响中序遍历。

提取区间

这是序列操作的基础(其实在某种程度上比线段树还好写)。

还记得删除操作吗?其实是一个道理的。例如我们要把区间\([l,r]\)提取出来,那么只需要把排名为\(l-1\)的点转到根节点,再把\(r+1\)转到根节点的儿子结点,根节点右边的就是区间\([l,n]\)(右边都比它大诶),\(r+1\)的左子树就是\([l,r]\)了。

刚接触时,我对于这些还是很疑惑的,为什么排名对应区间呢?排名不是根据权值调整的吗?
对于这个问题,我们要明白一件事,就是\(splay\)中排名实质上就是由我们插入的顺序决定的。回想上面的插入操作,我们过去在把\(splay\)作为普通平衡树使用时,只是刻意地把要插入的数调整到了相应位置,以此来保证我们插入的数保证平衡树的性质。
这里我把自己的问题分享出来,希望和大家共勉。

代码实现:

inline int gett(int l,int r){
	l=search(l-1);
	r=search(r+1);//找到排名对应的点的序号
	splay(l,0);
	splay(r,l);
	return son[r][0];//区间就包含在r的左子树中
}

翻转区间

模板题

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

翻转一个区间,实质就是把这个区间的所有左右子树交换位置。(试着手动模拟一下)

翻转一个指定的区间,我们只需要把这个区间先提取出来,再给这个子树的根节点打上一个翻转标记\(tag\)(参考线段树的标记思想)

最后在所有的操作完成后,对\(splay\)做一次中序遍历,把该下放的标记下放,最后输出中序遍历即可。

值得一提的是,我们在做旋转操作时,如果旋转了带有翻转标记的结点,那么结果会受到影响。为了避免这样的麻烦,我们就在\(search\)操作(根据排名找点)中,把碰到的每一个点的标记\(pushdown\),那么以后我们就可以为所欲为\(splay\)了。

理解后代码也很容易了

#include<bits/stdc++.h>
using namespace std;
#define ri register int
const int N=200001;
int n,m;
template<class T>inline void read(T &res){
    static char ch;T flag=1;
    while((ch=getchar())<'0'||ch>'9') if(ch=='-') flag=-1;
    res=ch-48;
    while((ch=getchar())>='0'&&ch<='9') res=(res<<1)+(res<<3)+ch-48;
    res*=flag;
}
class Splay{
    public: 
    int root=0,ndnum=0,son[N][2],fa[N],size[N],cnt[N],val[N];
    bool tag[N];
    inline void update(int p){
        size[p]=size[son[p][0]]+size[son[p][1]]+cnt[p];
    }
    inline void pushdown(int p){
        if(tag[p]){
            if(son[p][0]) tag[son[p][0]]^=1;
            if(son[p][1]) tag[son[p][1]]^=1;
            swap(son[p][0],son[p][1]);
            tag[p]^=1;
        }
    }
    inline bool check(int p){
        return p==son[fa[p]][1];
    }
    inline void rotate(int p){
        int f=fa[p],ff=fa[f],k=check(p),kk=check(f),nd=son[p][k^1];
        son[ff][kk]=p;fa[p]=ff;
        son[p][k^1]=f;fa[f]=p;
        son[f][k]=nd;fa[nd]=f;
        update(f);update(p);
    }
    inline void splay(int p,int goal){
        if(p==goal) return; 
        while(fa[p]!=goal){
            int f=fa[p],ff=fa[f];
            if(ff!=goal){
                if(check(p)==check(f))
                    rotate(f);
                else rotate(p);
            }
            rotate(p);
        }
        if(goal==0) root=p;
    }
    inline int search(int rk){
    	int p=root;
    	while(1){
    		pushdown(p);//标记下放
    		if(son[p][0]&&rk<=size[son[p][0]]) p=son[p][0];
    		else if(rk>size[son[p][0]]+cnt[p]){
    			rk-=size[son[p][0]]+cnt[p];
    			p=son[p][1];
            }
            else	return p;
        }
    }
    inline void insert(int x){
        int f=0,cur=root;
        while(cur&&x!=val[cur]){
            f=cur;
            cur=son[cur][x>val[cur]];
        }
        if(cur) ++cnt[cur];
        else{
            cur=++ndnum;
            fa[cur]=f;
            son[cur][0]=son[cur][1]=0;
            size[cur]=cnt[cur]=1;
            val[cur]=x;
            if(f) son[f][x>val[f]]=cur;
        }
        splay(cur,0);
    }
    inline void reverse(int l,int r){
        l=search(l);r=search(r+2);
        splay(l,0);splay(r,l);
        tag[son[r][0]]^=1;//区间在左儿子,打上标记,标记两次等于没标记
    }
    inline void bianli(int p){
        pushdown(p);
        if(son[p][0]) bianli(son[p][0]);
        if(val[p]<n+2&&val[p]>1)
            printf("%d ",val[p]-1);
        if(son[p][1]) bianli(son[p][1]);
    }
}Tree;
int main()
{
    read(n);read(m);
    for(ri i=1;i<=n+2;++i) Tree.insert(i);//前后个多插入一个数防爆,注意范围的移动
    for(ri i=1;i<=m;++i){
        int l,r;
        read(l);read(r);
        Tree.reverse(l,r);
    }
    Tree.bianli(Tree.root);
    return 0;
}

插入数到指定位置

这里只讲插入单个数,对于一段数的插入,有点麻烦 笔者水平有限

模板题

给定一个序列,初始为空。现在我们将1到N的数字插入到序列中,每次将一个数字插入到一个特定的位置。每插入一个数字,我们都想知道此时最长上升子序列长度是多少?

只讲插入操作,对于题目中求最长上升子序列请参考题解

把一个数\(x\)插入到位置\(pos\):把排名为\(pos-1\)的数转到根结点,把排名为\(pos\)的数转到根结点的儿子,显然\(pos\)的左子树为空,把\(x\)变为它的左儿子,即可完成操作。

代码实现:

inline void ins(int x,int pos){
	int l=search(pos-1),r=search(pos);
	splay(l,0);
	splay(r,l);
	son[r][0]=++ndnum;
	fa[ndnum]=r;
	cnt[ndnum]=size[ndnum]=1;
	val[ndnum]=x;
	splay(ndnum,0);
}
posted @ 2019-10-12 08:10  YZhe  阅读(411)  评论(0编辑  收藏  举报
ヾ(≧O≦)〃嗷~