【原创】/Restarting/ Splay树 (普通平衡树 文艺平衡树 bzoj1895 poj 2580 SuperMemo 题解)
Index
- Splay
- 說在前面
- splay樹的基本思路
- 基本的定義
- splay函數
- 旋轉 rotate
- 伸展 splay
- 插入 insert
- 前驅&后繼 pre&nxt
- 求數的排名和排名上的數
- 刪除 deleted
- 合并 join
- 分離 split
- 求最值 min&max
- 翻轉 turn
- 其他區間操作(以SuperMemo為例)
- 翻譯
- 一個個來
- 代碼
- 普通平衡樹
- 文藝平衡樹
- SuperMemo
- Splay的優缺點
- 參考文章
Splay
說在前面
關于二叉平衡樹是什么以及AVL樹的實現
用vector實現普通平衡樹!
qwq
標題好長!
真是聲勢浩大,徒有其表。
splay樹的基本思路
出于某些原因(cache原理),在訪問了某個節點之后,接下來有90%的概率很頻繁地再次訪問該節點,如果能把這個大概率會被多次訪問的結點放到離樹根盡可能近的地方,那么就可以節省不少的時間。
(大概如此)
所以要想辦法把最近訪問的結點扔到距離根節點盡可能近的位置。
著名計算機學家tarjan就想到了辦法。
基本的定義
不寫這個后文進行不下去啊。
const int MAXN=102030; struct Splay_Tree {int val,c[2],up;//c[0]代表左兒子,c[1]代表右兒子,up代表父親 }tree[MAXN];bool which(int pos) {return tree[tree[pos].up].c[1]==pos; }//返回pos是它父親的哪個兒子splay函數
splay的意思是伸展。
接下來給出的splay函數,能夠在保證一直保持著BST的結構的同時,把某個節點伸展到根去。
怎么做呢?
參考AVL樹,我們可以一點一點地把這個點旋轉上去。
旋轉 rotate
就以上圖為例子,假設被旋轉點在目標點的左下方。
現在,我們要把紅色點轉到它的父親橙色點的上面。
嗯哼哼(試圖吸引注意力),我是紅點。
rotate的基本思路就是,讓我右上方的父親(因為在右邊所以比我大)成為我的右兒子。
我父親之前在我的哪上方,那就讓他去我的哪下方。
然而這樣就會有另外三對父子關系收到了威脅。
也就是說……
我原來的右兒子(粉色)將何去何從?
我原來的爺爺(藍色)的兒子(橙色)怎么就沒了?
我原來的兄弟(紫色)的父親(就是我的父親橙色)怎么就沒了?
不要慌張,我們冷靜分析。
現在,
需要爸爸的:粉色 紫色 紅色
需要兒子的:橙色(一左一右) 藍色
正好都是三個,看來可以平均分。
這樣就沒問題了。
橙色點的右兒子還是它的右兒子(紫色)。
紅色點的右兒子(粉色)就接在新的右兒子(橙色)下面,當左兒子。
然后再讓紅色點接到原來的爺爺(藍色)下面。
(我寫了些什么?)
如果紅點在橙色點的右下角那就照照鏡子反過來。
也就是說,
如果我的父親在我的右上方,也就是我是我父親的左兒子。
那么就把我的父親拉下來成為我的新的右兒子。
此時,我的父親的左兒子就不是我了,我的右兒子的位置被擠占了(沒了和父親(我)的連接),我爺爺的兒子也沒有了。
于是讓我原來的右兒子成為我的原來的父親(現在新的右兒子)的左兒子,然后我篡權奪位,成為我原來爺爺的新兒子。
因此rotate函數可以這么寫:
void rotate(int pos) {int up=tree[pos].up,upup=tree[up].up,is=which(pos);//如果我是我父親的左兒子(is=0)的話,就讓我的右兒子當我父親的新的左兒子,我父親成為我的右兒子tree[up].c[is]=tree[pos].c[!is],tree[tree[pos].c[!is]].up=up;tree[pos].c[!is]=up,tree[up].up=pos;//爸爸認兒子的同時要記得兒子認爸爸啊//我的新爸爸就是我原來的爺爺,我原來的爺爺的新的兒子就是我tree[pos].up=upup;if(upup) tree[upup].c[tree[upup].c[1]==up]=pos;//當然如果爺爺是虛空(原來的爸爸就是根節點)的話,就不能爸爸認兒子了//還有還有,因為父子關系發生了說不清道不明的改變,所以這里不好用which,要用which一開始定義的時候的用 }伸展 splay
我們發現通過rotate我們能在不改變樹的平衡性的同時讓某個點上升一層,但是這離我們的目標(旋轉到根節點)還差得遠。
所以就有了splay操作:讓某個結點通過一次又一次rotate轉到根節點。比方說:
逆流而上的你眼前或許有無數曲折崎嶇道路,也許離終點遙遙無期,但是,
結點到達根源葉子結點\small 結點~~~~~~~~~~~到達根源~~~~~~~~~~~~~~~~~~~~~葉子結點結點???????????到達根源?????????????????????葉子結點
人一定要有夢想,沒有夢想和咸魚有什么區別?!~人一定要有夢想,沒有夢想和咸魚有什么區別?!?人一定要有夢想,沒有夢想和咸魚有什么區別?!
(這就是你四暗刻一向聽的時候碰碰杠杠做對對還一張dora沒有翻出來的原因?)
所以誰能告訴我這個“逆流而上的你”是什么啊還消不掉!(見下圖)
咳咳,話說回來,逆流而上的你眼前或許有無數曲折崎嶇道路,也許離終點遙遙無期,但是,絕無無法行走的路 (定義如此,走不了就不叫路了) ,只要你想要到達,就沒有無法克服的障礙,只要你想辦法的話。
在你一步一步往上rotate的時候,你的道路大概可以分為以下三類,六種:
還有一類沒有畫上去,就是爸爸就是根節點,沒有爺爺的情況,這個直接一個rotate就解決了。
折線形沒有什么好說的,一步一步rotate上去吧。
關于直線型:就是我是爸爸的a兒子,我爸爸是我爺爺的a兒子的情況。
科學家們告訴我們,這個時候應該先rotate(爸爸),再rotate(我)。
如果仍然是一直埋頭苦干rotate(我),這樣的自平衡方法叫做Spaly;而先rotate(父親),再rotate(我)的自平衡方法叫做splay。
如下圖:
乍一看差不多,甚至某道題目(BZOJ1036樹的統計Count)我把splay換成了spaly會快一些(5600ms->5000ms),但是咨詢了Freopen/Kyle/wk大佬后,Freopen/Kyle/wk告訴我,“可以證明splay更優,而且出數據的時候可以卡spaly。”
(所以說科學家等于wk?)
哇。
總結一下:
| 我爸爸是根,我沒有爺爺 | rotate(我) |
| 我,我爸爸,我爺爺呈一條直線 | rotate(父),rotate(我) |
| 我,我爸爸,我爺爺呈一條折線 | rotate(我),rotate(我) |
所以就可以得到splay函數:
void splay(int pos) {while(tree[pos].up){if(tree[tree[pos].up].up && which(tree[pos].up)==which(pos)) rotate(tree[pos].up);rotate(pos);}root=pos;//一個全局變量root來記錄splay樹的根 }當然,用splay操作可以使一個節點上升到它上面的任意一個結點
插入 insert
和正常的二叉平衡樹一樣,先找到對應的位置,直接插入,沒問題的。
然后再spaly到根節點上去。
前驅&后繼 pre&nxt
也有叫upper和lower的,還有等等名字。
和一般的二叉平衡樹沒有什么區別。
求數的排名和排名上的數
我稱之為:getrank()getrank()getrank()和rankget()rankget()rankget()。
如果采用惰性方法的話就很方便。
現在我們給每個節點新增兩個變量,cnt和siz。
cnt代表這個點上重復有多少個數。比方說val=1的點的cnt=4,就代表插入了4個1,都被塞到同一個點里。
siz代表子樹里數的個數(也就是說,不是子樹點的個數,而是子樹里各個點的cnt的值的和)
這樣子的話,插入和刪除會有些許變化。記得在樹的結構或者點的cnt改變的時候pushup一下來維護siz。(也就是insert和delete和splay的時候→也就是splay的時候)到時候給出普通平衡樹模板的時候一并看吧。
然后rankget,就是和找前驅差不多了,用找前驅的方法加上siz這個變量就可以輕松把前驅是誰轉換成求前驅有幾個的問題了。就是找到前驅然后把前驅的siz+1就是答案了。
然后是getrank,感覺像find和getrank的結合體,有了siz和cnt,我們就知道每個pos的val所對應的排名的區間是多少了。就是[tree[tree[pos].c[0]].siz+1,tree[tree[pos].c[0]].siz+tree[pos].cnt]\bold{\left[tree[tree[pos].c[0]].siz+1,tree[tree[pos].c[0]].siz+tree[pos].cnt\right]}[tree[tree[pos].c[0]].siz+1,tree[tree[pos].c[0]].siz+tree[pos].cnt],如果在這個范圍里,那么就找到了,如果給定的排名在這個排名區間的左邊,那就說明我們要找的數比當前的數要小,那么就向左二分下去,如果在右邊就向右邊。
需要稍微注意一下的是,向右邊搜的時候,給定的排名要減去(tree[tree[pos].c[0]].siz+tree[pos].cnt)(tree[tree[pos].c[0]].siz+tree[pos].cnt)(tree[tree[pos].c[0]].siz+tree[pos].cnt),因為當前節點的siz是它的左兒子+右兒子+自己,比方說左兒子和右兒子是[1,2,3,4,5,6,7]和[8,9,10]找第八名,當然應該在右兒子里面找了,但是右兒子只有3個數沒有第8名,所以把8-7(左兒子的siz)得到1,我們應該在右兒子里面查詢第1大的數。
刪除 deleted
因為delete這個函數名已經有了,所以加了一個d。
采用惰性刪除。
現在要分好幾種情況來討論。
首先,如果cnt-1之后還有剩余,就平安無事什么也不用干,cnt–就是了。
如果cnt==1,也就是刪除了這個點就沒有了:
(說實話空留一個cnt=0的點在那里浪費時間空間好像沒什么問題啊)
先把這個要刪除的東西splay到根節點處。
如果要刪除的點(目前已經splay到根了),沒有兒子: 這棵樹的最后的一個數被你刪了,這棵樹完了。
如果只有一個兒子:那么就直接把這個根節點移除掉,并把根節點的位置傳給那個兒子。
如果有兩個兒子的話:
把前驅找到并splay上來,然后把被刪除點的右兒子接到前驅的右邊,自己消失掉。
現在普通平衡樹的各個功能就寫好了。
然后是,
合并 join
有一顆Splay樹(記為S1)的所有節點都比另一顆Splay樹(記為S2)的最小的節點小的時候,
于是讓S1最大的節點Splay到S1的根,然后把S2接到S1的右下方。
好雞肋的功能。
圖來自楊思雨的論文。
分離 split
給定數x,把一顆splay樹分成兩棵樹,其中一棵樹的值都小于x,另一顆都大于x。
首先把x這個點splay到根,然后它的左子樹和右子樹即為所求。
求最值 min&max
這個就一直往左or右走就是了。
翻轉 turn
現在來考慮做文藝平衡樹。
文藝平衡樹要我們支持對一個數列進行區間翻轉再輸出。
首先,為了把用一棵樹來存一個數列,所以和普通的SBT不同(普通的SBT的中序遍歷是一個不下降序列)的,現在我們維護的splay樹的中序遍歷是這個區間本身。也就是從按權值不下降排序到下標不下降排序。
舉個例子就是一個數組{1,3,4,5,6,7,2,4,5,2},在一個普通的Splay樹中,它的中序遍歷是{1,2,2,3,4,4,5,5,6,7},在支持區間翻轉的Splay樹中,中序遍歷是**{1,3,4,5,6,7,2,4,5,2}**。
然后怎么區間翻轉呢?
先建一顆樹,按照題目所要求的,就假設N=12,那么數列就是{1,2,3,4,5,6,7,8,9,10,11,12},建成splay樹以后可以長這個樣子:
上圖的確是跑splay的時候跑出來的。
現在我們可以看到,中序遍歷就是{1,2,3,4,5,6,7,8,9,10,11,12},假設現在我們要翻轉區間[l,r],比方說[4,6],就是圖中綠點的區間:
我們先想辦法把這個區間給獨立出來。
那么我們先把r+1這個點Splay到根節點,也就是Splay(7)。
轉上來了。
然后再把l-1轉到r+1的左兒子,也就是Splay(3,tree[root].c[0])。
畢竟上文說了,可以把一個點splay到它上方任意一個節點,而它肯定在根節點的左側,那么根節點的左兒子一定在它的上方。
現在我們就把要操作的區間獨立出來了,就是根的左兒子的右子樹。(是一顆樹)
那么現在就可以做很多事情了。
比方說翻轉,對于這個獨立出來的子樹,要翻轉相當于交換每個節點的左右兒子,但是來如果要交換的話,那么就會很麻煩,況且一個區間被多次翻轉之后,很有可能翻轉回來,就浪費很多時間空間。
所以打懶標記吧。
標記一下這個點是否要翻轉左右兒子,輸出的時候如果有標記就翻轉地輸出。
然后每次翻轉區間的時候不需要對整個區間打標記,只需要在最上面的那個點那里打標記就行了。
如果要訪問這個區間里沒有打過標記的點,那么必然會訪問剛才打過標記的那個“最上面那個點”,那么在訪問那個點的時候就把標記下傳給兒子們,接下來訪問某個兒子,訪問這個兒子的時候再下傳給它的兒子……直到我們訪問到要找的那個點,此時它已經得到懶標記了,而整個過程幾乎沒有浪費時間在給暫時無關的結點打標記上。
代碼就丟在文藝平衡樹里面吧。
對了對了,因為要訪問l?1\bold {l-1}l?1和r+1\bold {r+1}r+1這兩個結點,所以為了不在翻轉區間[1,x]\bold {[1,x]}[1,x]或[x,n]\bold {[x,n]}[x,n]的時候爆掉,要在1號點之前加一個-inf,在n號點之后加一個inf,既然這樣那么哪個點對應哪個值就一定要想清楚了。
其他區間操作(以SuperMemo為例)
Your friend, Jackson is invited to a TV show called SuperMemo in which the participant is told to play a memorizing game. At first, the host tells the participant a sequence of numbers, A1,A2,...An{A1, A2, ... An}A1,A2,...An. Then the host performs a series of operations and queries on the sequence which consists:
ADDxyDADD ~x~ y ~DADD?x?y?D: Add D to each number in sub-sequence Ax...Ay{Ax ... Ay}Ax...Ay For example, performing “ADD241ADD ~2 ~4 ~1ADD?2?4?1” on 1,2,3,4,5{1, 2, 3, 4, 5}1,2,3,4,5results in 1,3,4,5,5{1, 3, 4, 5, 5}1,3,4,5,5
REVERSExyREVERSE ~x ~yREVERSE?x?y: reverse the sub-sequence Ax...Ay{Ax ... Ay}Ax...Ay. For example, performing “REVERSE24REVERSE ~2 ~4REVERSE?2?4” on 1,2,3,4,5{1, 2, 3, 4, 5}1,2,3,4,5 results in 1,4,3,2,5{1, 4, 3, 2, 5}1,4,3,2,5
REVOLVExyTREVOLVE ~x ~y ~TREVOLVE?x?y?T: rotate sub-sequence Ax...Ay{Ax ... Ay}Ax...Ay T times. For example, performing “REVOLVE242REVOLVE ~2~ 4 ~2REVOLVE?2?4?2” on 1,2,3,4,5{1, 2, 3, 4, 5}1,2,3,4,5 results in 1,3,4,2,5{1, 3, 4, 2, 5}1,3,4,2,5
INSERTxPINSERT~ x ~PINSERT?x?P: insert P after Ax. For example, performing “INSERT24INSERT~ 2~ 4INSERT?2?4” on 1,2,3,4,5{1, 2, 3, 4, 5}1,2,3,4,5 results in 1,2,4,3,4,5{1, 2, 4, 3, 4, 5}1,2,4,3,4,5
DELETExDELETE ~xDELETE?x: delete Ax. For example, performing “DELETE2DELETE ~2DELETE?2” on 1,2,3,4,5{1, 2, 3, 4, 5}1,2,3,4,5 results in 1,3,4,5{1, 3, 4, 5}1,3,4,5
MINxyMIN ~x~ yMIN?x?y: query the participant what is the minimum number in sub-sequence Ax...Ay{Ax ... Ay}Ax...Ay. For example, the correct answer to “MIN24MIN 2 ~4MIN2?4” on 1,2,3,4,5{1, 2, 3, 4, 5}1,2,3,4,5 is 222
To make the show more interesting, the participant is granted a chance to turn to someone else that means when Jackson feels difficult in answering a query he may call you for help. You task is to watch the TV show and write a program giving the correct answer to each query in order to assist Jackson whenever he calls.
翻譯
寫一個數據結構支持六種操作:
①ADDxyDADD~ x ~y~ DADD?x?y?D,對于區間[x,y][x,y][x,y]每個數都加上DDD。
②REVERSExyREVERSE ~x~ yREVERSE?x?y,翻轉區間[x,y][x,y][x,y]。
③REVOLVExyTREVOLVE ~x ~y ~TREVOLVE?x?y?T,這個厲害了,把區間[x,y][x,y][x,y]里的每個數在這個區間里面循環右移TTT次,舉個例子就是:1,2,3,4,5→5,1,2,3,4→4,5,1,2,3→3,4,5,1,2{1,2,3,4,5}\to {5,1,2,3,4} \to {4,5,1,2,3} \to {3,4,5,1,2}1,2,3,4,5→5,1,2,3,4→4,5,1,2,3→3,4,5,1,2
④INSERTxPINSERT~ x ~PINSERT?x?P,在xxx點的后面插入一個值為PPP的點。
⑤DELETExDELETE ~xDELETE?x,刪掉點xxx。
⑥MINxyMIN ~x~ yMIN?x?y,求區間[x,y][x,y][x,y]的最小值。
一個個來
對于ADD操作,先把這個區間獨立出來,然后打一個加法懶標記。
對于REVERSE操作,上面有。
對于REVOLVE操作,聲勢浩大,徒有其表,首先先把T%=(y-x+1);,那么就是把這個區間的后T個數移到前面y-x+1-T個數的前面;那么就是把前y-x+1-T個數REVERSE,把后T個數REVERSE,然后再把整個區間REVERSE就行了。
(這個字體的6寫得像4)
對于INSERT操作,因為它要求在某個點后面插入值,所以先把這個值xxx當成一個區間[x,x][x,x][x,x](數學考試這么寫是會被扣分的)把它獨立出來,也就是先把x+1x+1x+1 SplaySplaySplay到根節點,再把x?1x-1x?1 SplaySplaySplay到根節點的兒子,那么xxx就在x?1x-1x?1的右兒子,然后再把PPP接上去就是了。
對于DELETE操作,和INSERT一樣,把xxx獨立出來以后直接取下來就是了。
對于MIN操作,因為我們的樹不是按照數值大小關系來排序的,所以要額外開一個值來記錄子樹里的最小值,和siz一起push_up。
代碼
普通平衡樹
咕咕咕
文藝平衡樹
咕咕咕
SuperMemo
咕咕咕
Splay的優缺點
相較于AVL和Treap,Splay可以少存一個平衡因子。
Splay還有一個很重要的特性,那就是不穩定性,可能飚的很快,也可能被神秘卡掉。
所以“在嚴謹場合”不建議使用。
代碼實現比AVL要簡單一些。
參考文章
以下每一篇都比我的這個好:
伸展樹的基本操作與應用 IOI2004國家集訓隊 楊思雨
https://blog.csdn.net/a_comme_amour/article/details/79382104
https://www.cnblogs.com/cjyyb/p/7499020.html &https://blog.csdn.net/qq_30974369/article/details/77587168(并不詳細地講了spaly為什么會被卡)
https://blog.csdn.net/chenxiaoran666/article/details/81414567
http://www.cnblogs.com/dalt/p/8167168.html(時間復雜度分析)
https://blog.csdn.net/CABI_ZGX/article/details/82819882 (SuperMemo)
https://blog.csdn.net/DERITt/article/details/50485008 (更多的區間操作)
總結
以上是生活随笔為你收集整理的【原创】/Restarting/ Splay树 (普通平衡树 文艺平衡树 bzoj1895 poj 2580 SuperMemo 题解)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mtk 6577 root
- 下一篇: Liu C-2021-1: Nontri