Splay——學習筆記

@tocc++

代碼實現以及做爲基礎的平衡樹的應用

模板題數據結構

您須要寫一種數據結構(可參考題目標題),來維護一些數,其中須要提供如下操做:spa

插入xx數 刪除xx數(如有多個相同的數,因只刪除一個) 查詢xx數的排名(排名定義爲比當前數小的數的個數+1+1。如有多個相同的數,因輸出最小的排名) 查詢排名爲xx的數 求xx的前驅(前驅定義爲小於xx,且最大的數) 求xx的後繼(後繼定義爲大於xx,且最小的數)code

先貼個代碼get

代碼以下it

#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$一樣擁有維護一個序列的功能。io

此時,$splay$中的排名不在是具體的數的排名,而是對應了一個數列的相應的位置,它具有不少優良的性質,我會列舉幾個經常使用的功能。模板

首先,咱們要明確一件事,那就是咱們維護的區間在什麼地方。class

根據平衡樹的性質易得 咱們的區間就保存在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);
}
相關文章
相關標籤/搜索