洛谷 P3391 【模板】文藝平衡樹(Splay)


題目背景

這是一道經典的Splay模板題——文藝平衡樹。

題目描述

您需要寫一種數據結構(可參考題目標題),來維護一個有序數列,其中需要提供以下操作:翻轉一個區間,例如原有序序列是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次變換后的結果

輸入輸出樣例

輸入樣例#1: 
5 3
1 3
1 3
1 4
輸出樣例#1: 
4 3 2 1 5

說明

n,m≤100000 n, m

 

Splay的簡介:

 

1 簡介:
伸展樹,或者叫自適應查找樹,是一種用於保存有序集合的簡單高效的數據結構。伸展樹實質上是一個二叉查找樹。允許查找,插入,刪除,刪除最小,刪除最大,分割,合並等許多操作,這些操作的時間復雜度為O(logN)。由於伸展樹可以適應需求序列,因此他們的性能在實際應用中更優秀。
伸展樹支持所有的二叉樹操作。伸展樹不保證最壞情況下的時間復雜度為O(logN)。伸展樹的時間復雜度邊界是均攤的。盡管一個單獨的操作可能很耗時,但對於一個任意的操作序列,時間復雜度可以保證為O(logN)。

 

2 自調整和均攤分析:
    平衡查找樹的一些限制:
1、平衡查找樹每個節點都需要保存額外的信息。
2、難於實現,因此插入和刪除操作復雜度高,且是潛在的錯誤點。
3、對於簡單的輸入,性能並沒有什么提高。
    平衡查找樹可以考慮提高性能的地方:
1、平衡查找樹在最差、平均和最壞情況下的時間復雜度在本質上是相同的。
2、對一個節點的訪問,如果第二次訪問的時間小於第一次訪問,將是非常好的事情。
3、90-10法則。在實際情況中,90%的訪問發生在10%的數據上。
4、處理好那90%的情況就很好了。

 

3 均攤時間邊界:
在一顆二叉樹中訪問一個節點的時間復雜度是這個節點的深度。因此,我們可以重構樹的結構,使得被經常訪問的節點朝樹根的方向移動。盡管這會引入額外的操作,但是經常被訪問的節點被移動到了靠近根的位置,因此,對於這部分節點,我們可以很快的訪問。根據上面的90-10法則,這樣做可以提高性能。
為了達到上面的目的,我們需要使用一種策略──旋轉到根(rotate-to-root)。

  上面所說的,我來舉個例子解釋一下:假設有這樣一道題,有100000次操作,每次輸入a,b,若a為0表示將b放入數列中,若a為1表示輸出第b大的數。這道題看似簡單,不就直接二叉搜索樹嘛!但是如果數b是單調遞增出現的,則樹會成鏈,那么還是O(n^2)的復雜度。此時我們邊用到了Splay,由於splay是不斷翻轉的,所以就算某一時刻他成了一條鏈,也會馬上旋轉而變成另外的形態(深度減低),通過這樣不斷地變換可以防止長期停留在鏈的狀態,以保證每次操作平均復雜度O(log n)。

 

Splay的實現:

有話要說:

  關於Splay,我覺得自己已經完全掌握了,讓我口頭說還可以,但是要寫篇詳解實在是時間又少而且沒精力(而且大神們的博客已經寫的非常到位了,自己寫的肯定不及他們),所以這里我提供本人自學Splay時所看的一些比較有用的博客:1、基礎(非指針)    2、基礎(指針)   3、應用

認真看上述博客並思考,便會發現Splay其實很簡單。

 

Splay應用:

  Splay Tree可以方便的解決一些區間問題,根據不同形狀二叉樹先序遍歷結果不變的特性,可以將區間按順序建二叉查找樹。
每次自下而上的一套splay都可以將x移動到根節點的位置,利用這個特性,可以方便的利用Lazy的思想進行區間操作。
對於每個節點記錄size,代表子樹中節點的數目,這樣就可以很方便地查找區間中的第k小或第k大元素。
對於一段要處理的區間[x, y],首先splay x-1到root,再splay y+1到root的右孩子,這時root的右孩子的左孩子對應子樹就是整個區間。
  這樣,大部分區間問題都可以很方便的解決,操作同樣也適用於一個或多個條目的添加或刪除,和區間的移動。
  參考例題:bzoj3224
  操作:1. 插入x數
     2. 刪除x數(若有多個相同的數,因只刪除一個)
     3. 查詢x數的排名(若有多個相同的數,因輸出最小的排名)
     4. 查詢排名為x的數
     5. 求x的前驅(前驅定義為小於x,且最大的數)
     6. 求x的后繼(后繼定義為大於x,且最小的數)
 

關於這道題:

  只要我們弄懂Splay,其實本題很簡單:首先按照中序遍歷建樹,然后對於每次修改區間l,r,首先得提出這段區間,方法是將l的前趨l-1旋轉到根節點,將r的后趨r+1旋轉到根節點的右兒子,我們可以自己畫圖試試,容易發現經過這個操作后,根節點的右兒子的左子樹(具體應該說是這個左子樹的中序遍歷)就是區間l-r。關鍵的翻轉時,因為樹是中序遍歷(左根右),所以我們只要將l-r(前面所說的根節點的右兒子的左子樹)這個區間子樹左右兒子的節點交換位置(這樣再中序遍歷相當於右根左,即做到了翻轉操作)。關鍵是翻轉的優化,我們用到懶惰標記lazy[x](表示x是否翻轉),每次翻轉時只要某個節點有標記且在翻轉的區間內,則將標記下放給它的兩個兒子節點且將自身標記清0,這樣便避免了多余的重復翻轉。(不懂畫圖看博客)

1、裸代碼:

 

// luogu-judger-enable-o2
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define debug printf("%d %s\n",__LINE__,__FUNCTION__)
using namespace std;
const int N=100005;
il int gi()
{
    int a=0;char x=getchar();bool f=0;
    while((x<'0'||x>'9')&&x!='-')x=getchar();
    if(x=='-')x=getchar(),f=1;
    while(x>='0'&&x<='9')a=a*10+x-48,x=getchar();
    return f?-a:a;
}
int n,m,tot,root,siz[N],fa[N],flag[N],key[N],ch[N][2],cnt[N],ans[N];
il void update(int rt)
{
    int l=ch[rt][0],r=ch[rt][1];
    siz[rt]=siz[l]+siz[r]+1;
}
il void pushdown(int now)
{
    if(flag[now]){
        flag[ch[now][0]]^=1;
        flag[ch[now][1]]^=1;
        swap(ch[now][0],ch[now][1]);
        flag[now]=0;
    }
}
il int getson(int x){return ch[fa[x]][1]==x;}
il void rotate(int x)
{
    int y=fa[x],z=fa[y],b=getson(x),c=getson(y),a=ch[x][!b];
    if(z)ch[z][c]=x;else root=x;fa[x]=z;
    if(a)fa[a]=y;ch[y][b]=a;
    ch[x][!b]=y;fa[y]=x;
    update(y);update(x);
}
il void splay(int x,int i)
{
    while(fa[x]!=i){
        int y=fa[x],z=fa[y];
        if(z==i)rotate(x);
        else {
            if(getson(x)==getson(y)){rotate(y);rotate(x);}
            else {rotate(x);rotate(x);}
        }
    }
}
il int find(int x)
{
    int now=root;
    while(1){
        pushdown(now);
        if(ch[now][0]&&x<=siz[ch[now][0]])now=ch[now][0];
        else {
            int tmp=(ch[now][0]?siz[ch[now][0]]:0)+1;
            if(x<=tmp)return now;
            x-=tmp;
            now=ch[now][1];
        }
    }
}
il int build(int l,int r,int rt)
{
    int now=l+r>>1;
    fa[now]=rt;
    key[now]=ans[now];
    if(l<now)ch[now][0]=build(l,now-1,now);
    if(r>now)ch[now][1]=build(now+1,r,now);
    update(now);
    return now;
}
il void print(int now)
{
    pushdown(now);
    if(ch[now][0])print(ch[now][0]);
    ans[++tot]=key[now];
    if(ch[now][1])print(ch[now][1]);
}
int main()
{
    n=gi(),m=gi();int x,y;
    for(int i=1;i<=n+2;i++)ans[i]=i-1;
    root=build(1,n+2,0);
    for(int i=1;i<=m;i++){
        x=gi(),y=gi();
        x=find(x),y=find(y+2);
        splay(x,0);splay(y,x);
        flag[ch[ch[root][1]][0]]^=1;
    }
    print(root);
    for(int i=1;i<=n;i++)printf("%d ",ans[i+1]);
    return 0;
}

 

 

 

 

 2、方便理解,帶注釋代碼:

 

/*Splay只記模板是很困難的,而且真正運用時易生疏出錯,所以必須理解,在看代碼前先弄懂
Splay的原理,這篇代碼是帶注釋的Splay模板,題目來自洛谷P3391 ———————————by 520*/
#include<bits/stdc++.h>
#define il inline
#define debug printf("%d %s\n",__LINE__,__FUNCTION__)
using namespace std;
const int N=100005;
il int gi()
{
    int a=0;char x=getchar();bool f=0;
    while((x<'0'||x>'9')&&x!='-')x=getchar();
    if(x=='-')x=getchar(),f=1;
    while(x>='0'&&x<='9')a=a*10+x-48,x=getchar();
    return f?-a:a;
}

int n,m,tot,root,siz[N],fa[N],lazy[N],key[N],tree[N][2],ans[N];
/*root為根節點,siz存儲子樹節點數,fa存儲父節點,lazy是懶惰標記用來標記區間翻轉操作,key數組存儲原數列,tree為
splay樹,ans存儲答案*/

il void pushup(int rt)  //作用類似與線段樹
{
    int l=tree[rt][0],r=tree[rt][1];         //pushup作用是將子樹的節點個數更新給根節點
    siz[rt]=siz[l]+siz[r]+1;
}

il void pushdown(int now)
{
    if(lazy[now]){
        lazy[tree[now][0]]^=1;
        lazy[tree[now][1]]^=1;               /*pushdown作用是下放懶惰標記,若某一節點所在子樹(即某一區間)被翻轉
        ,則將懶惰標記下放給兩個兒子節點,交換左右兒子位置(中序遍歷,交換左右兒子后相當於翻轉)並對所在子樹根節
        點的標記清0,*/
        swap(tree[now][0],tree[now][1]);
        lazy[now]=0;
    }
}

il int getson(int x){return tree[fa[x]][1]==x;}  //getson判斷x是其父親的右兒子還是左兒子

il void rotate(int x)       //旋轉操作,直接寫在一個函數里,可以稱為上旋
{
    int y=fa[x],z=fa[y],b=getson(x),c=getson(y),a=tree[x][!b];  /*y是x的父節點,z是y的父節點,getson解釋過了。
        特別解釋一下a,a為旋轉時需要移動的子樹,若x為左兒子則右旋時要將x的右子樹移動,同理若x為右兒子則左旋時要
        將x的左子樹移動,所以這里a=tree[x][!b]*/
    if(z)tree[z][c]=x;else root=x;fa[x]=z; /*若z不為根節點,則用x替代y的位置;若z為根節點,則將x變為根節點。*/
    if(a)fa[a]=y;tree[y][b]=a; /*若存在要移動的子樹a,則把a和y相連,取代原來x的位置*/
    tree[x][!b]=y;fa[y]=x;  /*!b的原因:若x為左兒子則旋轉后y為x的右兒子,若x為右兒子則旋轉后y為x的左兒子。記得將y
                            指向x*/
    pushup(y);pushup(x);   /*旋轉后,對被移動了的y和x更新它們各自的子樹節點數*/
}

il void splay(int x,int i)
{
    while(fa[x]!=i){          //只要x沒有旋轉到需要的點下面,則一直旋,注意根節點的父親為虛點0
        int y=fa[x],z=fa[y];
        if(z==i)rotate(x);     //若x的爺爺是i,則只需旋一次
        else {    
            if(getson(x)==getson(y)){rotate(y);rotate(x);}   /*若x和y為相同偏向,則進行Zig-Zig或Zag-Zag操作*/
            else {rotate(x);rotate(x);}   /*否則進行Zig-Zag或Zag-Zig操作*/
                /*注意rotate函數中已經包含了這四種操作情況了*/
        }
    }
}

il int find(int x)    //查找x的位置
{
    int now=root;    //從根節點往下
    while(1){
        pushdown(now);    //本次操作要將前面的標記進行翻轉
        if(tree[now][0]&&x<=siz[tree[now][0]])now=tree[now][0];   //若存在左子樹且x小於等於左子樹的節點數,則x在左子樹上
        else {
            int tmp=(tree[now][0]?siz[tree[now][0]]:0)+1;   //往右子樹找,+1代表加上這個子樹的根節點
            if(x==tmp)return now;      //若找到了x,返回它的位置
            x-=tmp;      //否則x減去根節點右子樹以外的節點數,這個畫圖能理解,因為siz值並不是直接的x的值
            now=tree[now][1];  //將原來根節點的右兒子賦為新的根節點,繼續遞歸查找x位置
        }
    }
}

il int build(int l,int r,int rt)   //建樹過程和線段樹類似
{
    int now=l+r>>1;
    fa[now]=rt;
    key[now]=ans[now];        //key存原數組1到n,准確說是0到n+1,原因是主函數里的預處理
    if(l<now)tree[now][0]=build(l,now-1,now);
    if(r>now)tree[now][1]=build(now+1,r,now);
    pushup(now);   //記得pushup
    return now;
}

il void print(int now)   //輸出時中序遍歷,按左根右輸出
{
    pushdown(now);   //記得要翻轉
    if(tree[now][0])print(tree[now][0]);   //因為中序遍歷左根右,所以遞歸根節點左子樹到第一個數的位置
    ans[++tot]=key[now];   //回溯時存儲答案,注意我們翻轉操作的是原數組下標
    if(tree[now][1])print(tree[now][1]);   //同理遞歸根節點的右子樹
}

int main()
{
    n=gi(),m=gi();int x,y;
    for(int i=1;i<=n+2;i++)ans[i]=i-1;    /*因為取出操作區間時旋轉的是x的前驅和y的后驅,所以預處理時第i個點
        存的是i的前驅*/
    root=build(1,n+2,0);
    while(m--)
    {
        x=gi(),y=gi();
        x=find(x),y=find(y+2);  /*查找x的前驅所在的位置,和y后驅所在的位置,因為預處理時ans存的是前趨,
                                所以直接查找x,而y的后驅變成了y+2*/
        splay(x,0);splay(y,x);  /*將x前驅上旋至根節點,y的后驅上旋成根節點右兒子的左子樹*/
        lazy[tree[tree[root][1]][0]]^=1;//經過旋轉后,此時根節點的右兒子的左子樹就是需要翻轉的區間,所以lazy標記
    }
    print(root);
    for(int i=1;i<=n;i++)printf("%d ",ans[i+1]);   //輸出時將前驅還原為原數
    return 0;
}

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM