這是一道經典的Splay模板題——文藝平衡樹。html
您須要寫一種數據結構(可參考題目標題),來維護一個有序數列,其中須要提供如下操做:翻轉一個區間,例如原有序序列是5 4 3 2 1,翻轉區間是[2,4]的話,結果是5 2 3 4 1c++
第一行爲n,m n表示初始序列有n個數,這個序列依次是(1,2,⋯n−1,n) m表示翻轉操做次數數組
接下來m行每行兩個數 [l,r]數據保證 1≤l≤r≤n數據結構
輸出格式:輸出一行n個數字,表示原始序列通過m次變換後的結果函數
5 3
1 3
1 3
1 4
4 3 2 1 5
n,m≤100000 n, m性能
1 簡介:
伸展樹,或者叫自適應查找樹,是一種用於保存有序集合的簡單高效的數據結構。伸展樹實質上是一個二叉查找樹。容許查找,插入,刪除,刪除最小,刪除最大,分割,合併等許多操做,這些操做的時間複雜度爲O(logN)。因爲伸展樹能夠適應需求序列,所以他們的性能在實際應用中更優秀。
伸展樹支持全部的二叉樹操做。伸展樹不保證最壞狀況下的時間複雜度爲O(logN)。伸展樹的時間複雜度邊界是均攤的。儘管一個單獨的操做可能很耗時,但對於一個任意的操做序列,時間複雜度能夠保證爲O(logN)。優化
2 自調整和均攤分析:
平衡查找樹的一些限制:
一、平衡查找樹每一個節點都須要保存額外的信息。
二、難於實現,所以插入和刪除操做複雜度高,且是潛在的錯誤點。
三、對於簡單的輸入,性能並無什麼提升。
平衡查找樹能夠考慮提升性能的地方:
一、平衡查找樹在最差、平均和最壞狀況下的時間複雜度在本質上是相同的。
二、對一個節點的訪問,若是第二次訪問的時間小於第一次訪問,將是很是好的事情。
三、90-10法則。在實際狀況中,90%的訪問發生在10%的數據上。
四、處理好那90%的狀況就很好了。ui
3 均攤時間邊界:
在一顆二叉樹中訪問一個節點的時間複雜度是這個節點的深度。所以,咱們能夠重構樹的結構,使得被常常訪問的節點朝樹根的方向移動。儘管這會引入額外的操做,可是常常被訪問的節點被移動到了靠近根的位置,所以,對於這部分節點,咱們能夠很快的訪問。根據上面的90-10法則,這樣作能夠提升性能。
爲了達到上面的目的,咱們須要使用一種策略──旋轉到根(rotate-to-root)。spa
上面所說的,我來舉個例子解釋一下:假設有這樣一道題,有100000次操做,每次輸入a,b,若a爲0表示將b放入數列中,若a爲1表示輸出第b大的數。這道題看似簡單,不就直接二叉搜索樹嘛!可是若是數b是單調遞增出現的,則樹會成鏈,那麼仍是O(n^2)的複雜度。此時咱們邊用到了Splay,因爲splay是不斷翻轉的,因此就算某一時刻他成了一條鏈,也會立刻旋轉而變成另外的形態(深度減低),經過這樣不斷地變換能夠防止長期停留在鏈的狀態,以保證每次操做平均複雜度O(log n)。.net
關於Splay,我以爲本身已經徹底掌握了,讓我口頭說還能夠,可是要寫篇詳解實在是時間又少並且沒精力(並且大神們的博客已經寫的很是到位了,本身寫的確定不及他們),因此這裏我提供本人自學Splay時所看的一些比較有用的博客:一、基礎(非指針) 二、基礎(指針) 三、應用
認真看上述博客並思考,便會發現Splay其實很簡單。
只要咱們弄懂Splay,其實本題很簡單:首先按照中序遍歷建樹,而後對於每次修改區間l,r,首先得提出這段區間,方法是將l的前趨l-1旋轉到根節點,將r的後趨r+1旋轉到根節點的右兒子,咱們能夠本身畫圖試試,容易發現通過這個操做後,根節點的右兒子的左子樹(具體應該說是這個左子樹的中序遍歷)就是區間l-r。關鍵的翻轉時,由於樹是中序遍歷(左根右),因此咱們只要將l-r(前面所說的根節點的右兒子的左子樹)這個區間子樹左右兒子的節點交換位置(這樣再中序遍歷至關於右根左,即作到了翻轉操做)。關鍵是翻轉的優化,咱們用到懶惰標記lazy[x](表示x是否翻轉),每次翻轉時只要某個節點有標記且在翻轉的區間內,則將標記下放給它的兩個兒子節點且將自身標記清0,這樣便避免了多餘的重複翻轉。(不懂畫圖看博客)
一、裸代碼:
// 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; }
二、方便理解,帶註釋代碼:
/*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; }