Splay總結

伸展樹(Splay)

Tags:數據結構html

更好閱讀體驗:https://www.zybuluo.com/xzyxzy/note/1004417


1、基本內容

yyb博客:http://www.cnblogs.com/cjyyb/p/7499020.html
基本類型:平衡樹(樹深度指望是log的)二叉搜索樹(中序遍歷爲從小到大)
核心思想:不斷把查詢過的點轉到根,儘量打亂順序使其儘可能接近指望複雜度
(蘿蔔說研究代表90%的詢問都集中在10%的數據中,因此Splay十分難被卡)ios


2、題目

一、練基礎

P3369 【模板】普通平衡樹 https://www.luogu.org/problemnew/show/3369
P2286 [HNOI2004]寵物收養場 https://www.luogu.org/problemnew/show/2286
P2234 [HNOI2002]營業額統計 https://www.luogu.org/problemnew/show/2234
P2073 送花 https://www.luogu.org/problemnew/show/P2073算法

二、刷提升

P3224 [HNOI2012]永無鄉 https://www.luogu.org/problemnew/show/3224
P1486 [NOI2004]鬱悶的出納員 https://www.luogu.org/problemnew/show/P1486
P3391 【模板】文藝平衡樹https://www.luogu.org/problemnew/show/P3391
P2596 [ZJOI2006]書架 https://www.luogu.org/problemnew/show/P2596
P2584 [ZJOI2006]GameZ遊戲排名系統 https://www.luogu.org/show/P2584
P3201 [HNOI2009]夢幻布丁(題解) https://www.luogu.org/show/3201數組

三、變態題

P3165 [CQOI2014]排序機械臂 https://www.luogu.org/problemnew/show/P3165
P2042 [NOI2005]維護數列 https://www.luogu.org/problemnew/show/P2042數據結構


3、作題經驗

一、支持的操做

對於點

A、插/刪點函數

詳見後方區間操做ui

B、動態查詢某數(結點)前驅後繼spa

用Next函數實現,原理是把須要查詢的數轉到根,再在其右子樹的最左端查後繼,左子樹的最右段查前驅調試

C、查詢排名爲k的數/查詢k的排名code

k的排名那麼把k轉到根,左子樹大小即是k的排名,見代碼110-120行,很好理解

對於區間

每次將要訪問到兒子結點的時候都要pushdown!!

A、插/刪區間

把要插入的位置的前一個數繞到根,後一個數繞到根的右兒子,而後把插入的區間建成一棵splay連到後一個數的左兒子上
\(l\)的前驅轉到根,\(r\)後繼轉到\(l\)前驅的兒子,而後後繼的左兒子那一段就是要刪的點,直接去掉

B、區間翻轉

翻轉一個區間,顛覆了Splay按權值排序的概念
具體操做就是翻轉一個區間,至關於把全部結點的左右兒子交換
那麼打一個標記,當要修改時pushdown就行了(好像和線段樹了懶標記有點像哦)
建議把標記定義成已經翻轉了該點的左右兒子!這裏是模板!這裏有難題!

C、區間覆蓋

D、區間求和

E、求數列最大子段和

詳見下方維護數列的code

F、區間加法

本身yy一下應該能夠弄出來

因而可知,splay能夠代替線段樹的全部操做!
可是,越通用的算法常數越大!
大體能夠理解爲 splay>線段樹>樹狀數組

還有呢

Splay合併

這個嘛不太好說,啓發式合併好了,夢幻布丁和永無鄉都是的

Splay模板

//https://www.luogu.org/problemnew/show/P3369
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
using namespace std;
int read()
{
    char ch=getchar();
    int h=0,t=1;
    while(ch!='-'&&(ch>'9'||ch<'0'))ch=getchar();
    if(ch=='-'){ch=getchar();t=-1;}
    while(ch>='0'&&ch<='9'){h=h*10+ch-'0';ch=getchar();}
    return h*t;
}
int n,tot,root;
struct Splay{int fa,val,cnt,siz,ch[2];}t[100011];
void pushup(int x)
{
    t[x].siz=t[t[x].ch[0]].siz+t[t[x].ch[1]].siz+t[x].cnt;
}
void rotate(int x)//把x的父親變成它兒子
{
    int y=t[x].fa,z=t[y].fa;
    int k=t[y].ch[1]==x;//表示x是y的 0左兒子 1右兒子
    //Step1 把x和z相連(相同)
    t[z].ch[t[z].ch[1]==y]=x;//y位置
    t[x].fa=z;
    //Step2 把x的兒子丟給y(相反)
    t[y].ch[k]=t[x].ch[k^1];
    t[t[x].ch[k^1]].fa=y;
    //Step3 把y搞成x的兒子(相反)
    t[x].ch[k^1]=y;
    t[y].fa=x;
    pushup(y);//更新!!
    //注意順序!!!!!
}
void splay(int x,int fa)//把x旋轉成fa的兒子(fa=0則旋轉到根)
{
    while(t[x].fa!=fa)
    {
        int y=t[x].fa,z=t[y].fa;
        if(z!=fa)(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
        rotate(x);
    }
    if(!fa)root=x;
    pushup(x);//更新!!(放在這裏常數會小些)
    /*
      這是重點!敲黑板!
      把x旋轉到根,這一步保證了雙旋,近似能夠理解爲rand了一下鏈(能夠本身畫一下單旋,會發現單旋的舊鏈沒有改變,複雜度會被卡)因而這樣以後更接近了指望複雜度NlogN
      雙旋要求x祖父不是將要成爲x父親的結點(若是是的並且旋轉了y那麼z成爲y兒子,x永遠沒法成爲z的兒子),並且當從祖父到x一直是左兒子或者一直是右兒子就能夠轉y了,而且每一步最後都必須動x,在前面選擇的時候,若是不一樣時爲左兒子或右兒子,那麼動y並不會將x向上提,沒有效果
      在每一步最後,若是不動x而是一直動y,那麼y上方的舊鏈會出如今y右兒子的左兒子上
      每一步都是精心打造,目的是讓樹儘量隨機重構,來平衡其指望複雜度,若是有任何疑問,手玩一棵Splay就會發現去掉某一句都會使Splay出現舊鏈或效率變低
    */
}
void Insert(int num)//把num加入Splay樹
{
    int x=root,fa=0;
    while(x&&t[x].val!=num)
    {
        fa=x;//向下找
        x=t[x].ch[num>t[x].val];//大於向右,小於向左
    }
    if(x){t[x].cnt++;splay(x,0);return;}//新點非根
    x=++tot;
    if(fa)t[fa].ch[num>t[fa].val]=x;
    t[x]=(Splay){fa,num,1,1,{0,0}};
    splay(x,0);//把當前位置移到根,保證結構的平衡
    //還有一個做用,更新了x的樹大小,那麼要一路更新上去
}
void find(int num)//找到num的位置並把它旋轉到根
{
    int x=root;if(!x)return;
    while(t[x].ch[num>t[x].val]&&num!=t[x].val)
        x=t[x].ch[num>t[x].val];
    splay(x,0);//旋轉到根
}
int Next(int num,int f)//查找num的前驅(0)或後繼(1)
{
    find(num);int x=root;
    if(t[x].val>num&&f)return x;//當前結點大於x且查詢後繼
    if(t[x].val<num&&!f)return x;//當前結點小於x且查詢前驅
    x=t[x].ch[f];//後繼在右子樹,前驅在左子樹
    while(t[x].ch[f^1])x=t[x].ch[f^1];//反着找
    return x;
}
void Delete(int num)//刪除num(同理也能夠刪除區間)
{
    int last=Next(num,0),next=Next(num,1);
    splay(last,0);splay(next,last);
    //查找l的前驅和r的後繼,把前驅轉到根,後繼轉到根的下面
    //那麼l到r這段區間裏全部數就是在後繼的左兒子上了
    int pos=t[next].ch[0];
    if(t[pos].cnt>1)
    {
        t[pos].cnt--;
        splay(pos,0);
    }
    else
    {
        t[next].ch[0]=0;//丟掉!
        pushup(next);pushup(last);//要記得更新呦~
    }
}
int Query1Rank(int num)//查找num的編號
{
    find(num);
    return t[t[root].ch[0]].siz;
}
int Query2Rank(int num)//查找編號爲num的數
{
    int x=root;if(t[x].siz<num)return 0;
    while(1)
    {
        int Size=t[t[x].ch[0]].siz,Cnt=t[x].cnt;
        if(num>Size&&num<=Size+Cnt)return t[x].val;
        if(num<=Size)x=t[x].ch[0];
        if(num>Size+Cnt){num-=Size+Cnt;x=t[x].ch[1];}//注意順序
    }
}
int main()
{
    n=read();
    Insert(+2147483647);
    Insert(-2147483647);//便於找到前驅後繼
    for(int i=1;i<=n;i++)
    {
        int opt=read(),x=read();
        if(opt==1){Insert(x);}
        if(opt==2){Delete(x);}
        if(opt==3){printf("%d\n",Query1Rank(x));}
        if(opt==4){printf("%d\n",Query2Rank(x+1));}
        if(opt==5){printf("%d\n",t[Next(x,0)].val);}
        if(opt==6){printf("%d\n",t[Next(x,1)].val);}
    }
    return 0;
}

維護數列

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<queue>
#include<cstring>
#define RG register 
using namespace std;
inline int read()
{
    RG char ch=getchar();
    RG int h=0,t=1;
    while(ch!='-'&&(ch>'9'||ch<'0'))ch=getchar();
    if(ch=='-'){t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){h=(h<<3)+(h<<1)+ch-'0';ch=getchar();}
    return h*t;
}
const int MAXN=500100;
int N,M,root,tot;
struct Splay
{
    int val,fa,siz,ch[2];
    int sum,lx,rx,mx;
    bool mark,lazy; 
    //bool u;
}t[MAXN];
queue<int>Q;//內存池
int pos,all,c;
int a[MAXN],zhan[MAXN],top;
/*
inline void Printtree()
//功能:調試輸出Splay
{
    tot=0;
    printf("root=%d\n",root);
    for(RG int i=1;i<=MAXN-1;i++)
        if(t[i].u)
            tot++,printf("#%2d: fa=%2d,siz=%2d,mark=%2d,lazy=%2d,val=%2d,sum=%2d,lc=%2d,rc=%2d,lx=%2d,rx=%2d,mx=%2d\n",i,t[i].fa,t[i].siz,t[i].mark,t[i].lazy,t[i].val,t[i].sum,t[i].ch[0],t[i].ch[1],t[i].lx,t[i].rx,t[i].mx);
    printf("tot=%d\n",tot);
}
*/
inline Splay Get(RG int v,RG int f,RG int u)
//功能:見下,初始化一個結點
{
    RG Splay R;
    R.val=v;R.fa=f;R.siz=u;R.ch[0]=0;R.ch[1]=0;
    R.mark=0;R.lazy=0;R.sum=v;
    R.lx=max(0,v);R.rx=max(0,v);R.mx=u?v:-1e9;
    //R.u=u;
    return R;
}
inline void pushup(RG int x)
//功能:由下往上更新x結點的一些內容
{
    t[x].siz=t[t[x].ch[0]].siz+t[t[x].ch[1]].siz+1;
    t[x].sum=t[t[x].ch[0]].sum+t[t[x].ch[1]].sum+t[x].val;
    t[x].lx=max(t[t[x].ch[0]].lx,t[t[x].ch[0]].sum+t[x].val+t[t[x].ch[1]].lx);
    t[x].rx=max(t[t[x].ch[1]].rx,t[t[x].ch[1]].sum+t[x].val+t[t[x].ch[0]].rx);
    t[x].mx=max(max(t[t[x].ch[0]].mx,t[t[x].ch[1]].mx),t[t[x].ch[0]].rx+t[x].val+t[t[x].ch[1]].lx);
    //留坑:當一個點沒有右兒子而後mx必須爲負數的時候,由此程序跑出來mx=0,但已經過的程序大部分都沒判,故留坑
}
inline void pushdown(RG int x)
//功能:由上往下下放一些標記
{
    if(t[x].lazy)//lazy表示已經改變了當前結點的值
    {
        t[x].lazy=0;t[x].mark=0;
        if(t[x].ch[0]){t[t[x].ch[0]].val=t[x].val;t[t[x].ch[0]].sum=t[t[x].ch[0]].siz*t[x].val;t[t[x].ch[0]].lazy=1;}
        if(t[x].ch[1]){t[t[x].ch[1]].val=t[x].val;t[t[x].ch[1]].sum=t[t[x].ch[1]].siz*t[x].val;t[t[x].ch[1]].lazy=1;}
        if(t[x].val>=0)
        {
            if(t[x].ch[0]){t[t[x].ch[0]].lx=t[t[x].ch[0]].rx=t[t[x].ch[0]].mx=t[t[x].ch[0]].sum;}
            if(t[x].ch[1]){t[t[x].ch[1]].lx=t[t[x].ch[1]].rx=t[t[x].ch[1]].mx=t[t[x].ch[1]].sum;}
        }
        else
        {
            if(t[x].ch[0]){t[t[x].ch[0]].lx=t[t[x].ch[0]].rx=0;t[t[x].ch[0]].mx=t[x].val;}
            if(t[x].ch[1]){t[t[x].ch[1]].lx=t[t[x].ch[1]].rx=0;t[t[x].ch[1]].mx=t[x].val;}
        }
    }
    if(t[x].mark)//mark表示已經交換了當前結點的左右兒子
    {
        t[x].mark=0;
        if(t[x].ch[0])t[t[x].ch[0]].mark^=1;
        if(t[x].ch[1])t[t[x].ch[1]].mark^=1;
        swap(t[t[x].ch[0]].lx,t[t[x].ch[0]].rx);
        swap(t[t[x].ch[1]].lx,t[t[x].ch[1]].rx);
        //Attention:上面左兒子和右兒子的左右右子段交換,畫圖稍微模擬一下
        swap(t[t[x].ch[0]].ch[0],t[t[x].ch[0]].ch[1]);
        swap(t[t[x].ch[1]].ch[0],t[t[x].ch[1]].ch[1]);      
    }
}
inline void Find(RG int x)
//功能:把從root到x的路徑一直pushdown
{
    top=0;zhan[++top]=x;
    if(x==root){pushdown(x);return;}
    while(t[x].fa!=root){zhan[++top]=t[x].fa;x=t[x].fa;}
    zhan[++top]=root;
    while(top){pushdown(zhan[top]);top--;}
}
inline void rotate(RG int x)
//功能:把x旋轉成x父親的父親
{
    RG int y=t[x].fa,z=t[y].fa;
    RG int k=t[y].ch[1]==x;
    t[z].ch[t[z].ch[1]==y]=x;  t[x].fa=z;
    t[y].ch[k]=t[x].ch[k^1];   t[t[x].ch[k^1]].fa=y;
    t[x].ch[k^1]=y;            t[y].fa=x;
    pushup(y);
}
inline void splay(RG int x,RG int fa)
//功能:把x旋轉成爲fa的兒子
{
    Find(x);
    while(t[x].fa!=fa)
    {
        RG int y=t[x].fa,z=t[y].fa;
        if(z!=fa)(t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
        rotate(x);
    }
    if(!fa)root=x;
    pushup(x);
}
inline int Buildtree(RG int l,RG int r,RG int fa)
//功能:a[l]到a[r]之間創建以fa爲根的父親的Splay並返回其根的結點編號
{
    if(l>r)return 0;
    RG int x=Q.front();Q.pop();//從內存池中取出編號
    if(l==r){t[x]=Get(a[l],fa,1);return x;}
    RG int mid=(l+r)>>1;
    t[x]=Get(a[mid],fa,1);
    t[x].ch[0]=Buildtree(l,mid-1,x);
    t[x].ch[1]=Buildtree(mid+1,r,x);
    pushup(x);return x;
}
inline int Kth(RG int num)
//功能:在Splay中找到第num個數並返回結點編號
{
    RG int x=root;
    while(1)
    {
        pushdown(x);
        RG int Size=t[t[x].ch[0]].siz;
        if(num<=Size)x=t[x].ch[0];
        if(num==Size+1)return x;
        if(num>Size+1){num-=Size+1;x=t[x].ch[1];}
    }
}
inline void Insert(RG int pos,RG int all)
//功能:在第pos+1個數後插入all個數(哨兵影響)
{
    for(RG int i=1;i<=all;i++)a[i]=read();
    RG int x=Kth(pos+1),next=Kth(pos+2);
    splay(x,0);//pushdown(x);
    splay(next,x);//pushdown(next);
    t[next].ch[0]=Buildtree(1,all,next);
    pushup(next);pushup(x);
}
inline void Recycle(RG int x)
//功能:回收以x爲根的子樹中全部結點
{
    if(!x)return;
    if(t[x].ch[0])Recycle(t[x].ch[0]);
    if(t[x].ch[1])Recycle(t[x].ch[1]);
    t[x]=Get(0,0,0);Q.push(x);
}
inline void Work(RG int pos,RG int all,RG int op)
//功能:表示對區間[pos+1,pos+all]的操做(因爲兩個哨兵)
//      op=1刪除   op=2區間覆蓋
//      op=3翻轉   op=4求和
{
    if(all==0){if(op==2)read();if(op==4)printf("0\n");return;}
    //printf("[%d,%d]進行%d\n",pos+1,pos+all,op);
    RG int last=Kth(pos),next=Kth(pos+all+1);
    splay(last,0);//pushdown(last);
    splay(next,last);//pushdown(next);
    //這裏不須要由於splay(x)的時候已經pushdown(x)了
    RG int x=t[next].ch[0];
    if(op==1)
    {
        Recycle(x);
        t[next].ch[0]=0;
    }
    if(op==2)
    {
        c=read();t[x].lazy=1;
        t[x].val=c;t[x].sum=t[x].siz*c;
        if(c>=0){t[x].lx=t[x].rx=t[x].mx=t[x].sum;}
        else{t[x].lx=t[x].rx=0;t[x].mx=c;}
    }
    if(op==3&&!t[x].lazy)
    {
        t[x].mark^=1;
        swap(t[x].lx,t[x].rx);
        swap(t[x].ch[0],t[x].ch[1]);
    }
    if(op==4)printf("%d\n",t[x].sum);
    pushup(next);pushup(last);
}
int main()
{
    freopen("seq2005.in","r",stdin);
    freopen("seq2005.out","w",stdout);
    N=read();M=read();
    t[0].mx=a[1]=a[N+2]=-1e9;
    t[0].val=t[0].fa=t[0].siz=t[0].mark=t[0].lazy=0;
    t[0].sum=t[0].ch[0]=t[0].ch[1]=t[0].lx=t[0].rx=0;   
    for(RG int i=1;i<=MAXN-1;i++)Q.push(i);
    for(RG int i=1;i<=N;i++)a[i+1]=read();//左右哨兵
    root=Buildtree(1,N+2,0);
    for(RG int i=1;i<=M;i++)
    {
        RG char s[20];scanf("%s",s);
        //printf("%s\n",s);
        if(s[0]!='M'||s[2]=='K'){pos=read();all=read();}
        if(s[0]=='I')Insert(pos,all);//在pos後加入all個數
        if(s[0]=='D')Work(pos,all,1);//在pos後刪去all個數
        if(s[0]=='M')
        {
            if(s[2]=='K')Work(pos,all,2);//區間覆蓋
            else printf("%d\n",t[root].mx);//最大子段和
        }
        if(s[0]=='R')Work(pos,all,3);//翻轉
        if(s[0]=='G')Work(pos,all,4);//求和
        //Printtree();
    }
    return 0;
}
相關文章
相關標籤/搜索