【原创】/Restarting/ Splay树 (普通平衡树 文艺平衡树 bzoj1895 poj 2580 SuperMemo 题解)
Index
- Splay
- 說在前面
- splay樹的基本思路
- 基本的定義
- splay函數(shù)
- 旋轉(zhuǎn) rotate
- 伸展 splay
- 插入 insert
- 前驅(qū)&后繼 pre&nxt
- 求數(shù)的排名和排名上的數(shù)
- 刪除 deleted
- 合并 join
- 分離 split
- 求最值 min&max
- 翻轉(zhuǎn) turn
- 其他區(qū)間操作(以SuperMemo為例)
- 翻譯
- 一個個來
- 代碼
- 普通平衡樹
- 文藝平衡樹
- SuperMemo
- Splay的優(yōu)缺點
- 參考文章
Splay
說在前面
關(guān)于二叉平衡樹是什么以及AVL樹的實現(xiàn)
用vector實現(xiàn)普通平衡樹!
qwq
標(biāo)題好長!
真是聲勢浩大,徒有其表。
splay樹的基本思路
出于某些原因(cache原理),在訪問了某個節(jié)點之后,接下來有90%的概率很頻繁地再次訪問該節(jié)點,如果能把這個大概率會被多次訪問的結(jié)點放到離樹根盡可能近的地方,那么就可以節(jié)省不少的時間。
(大概如此)
所以要想辦法把最近訪問的結(jié)點扔到距離根節(jié)點盡可能近的位置。
著名計算機學(xué)家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函數(shù)
splay的意思是伸展。
接下來給出的splay函數(shù),能夠在保證一直保持著BST的結(jié)構(gòu)的同時,把某個節(jié)點伸展到根去。
怎么做呢?
參考AVL樹,我們可以一點一點地把這個點旋轉(zhuǎn)上去。
旋轉(zhuǎn) rotate
就以上圖為例子,假設(shè)被旋轉(zhuǎn)點在目標(biāo)點的左下方。
現(xiàn)在,我們要把紅色點轉(zhuǎn)到它的父親橙色點的上面。
嗯哼哼(試圖吸引注意力),我是紅點。
rotate的基本思路就是,讓我右上方的父親(因為在右邊所以比我大)成為我的右兒子。
我父親之前在我的哪上方,那就讓他去我的哪下方。
然而這樣就會有另外三對父子關(guān)系收到了威脅。
也就是說……
我原來的右兒子(粉色)將何去何從?
我原來的爺爺(藍色)的兒子(橙色)怎么就沒了?
我原來的兄弟(紫色)的父親(就是我的父親橙色)怎么就沒了?
不要慌張,我們冷靜分析。
現(xiàn)在,
需要爸爸的:粉色 紫色 紅色
需要兒子的:橙色(一左一右) 藍色
正好都是三個,看來可以平均分。
這樣就沒問題了。
橙色點的右兒子還是它的右兒子(紫色)。
紅色點的右兒子(粉色)就接在新的右兒子(橙色)下面,當(dāng)左兒子。
然后再讓紅色點接到原來的爺爺(藍色)下面。
(我寫了些什么?)
如果紅點在橙色點的右下角那就照照鏡子反過來。
也就是說,
如果我的父親在我的右上方,也就是我是我父親的左兒子。
那么就把我的父親拉下來成為我的新的右兒子。
此時,我的父親的左兒子就不是我了,我的右兒子的位置被擠占了(沒了和父親(我)的連接),我爺爺?shù)膬鹤右矝]有了。
于是讓我原來的右兒子成為我的原來的父親(現(xiàn)在新的右兒子)的左兒子,然后我篡權(quán)奪位,成為我原來爺爺?shù)男聝鹤印?/p>
因此rotate函數(shù)可以這么寫:
void rotate(int pos) {int up=tree[pos].up,upup=tree[up].up,is=which(pos);//如果我是我父親的左兒子(is=0)的話,就讓我的右兒子當(dāng)我父親的新的左兒子,我父親成為我的右兒子tree[up].c[is]=tree[pos].c[!is],tree[tree[pos].c[!is]].up=up;tree[pos].c[!is]=up,tree[up].up=pos;//爸爸認兒子的同時要記得兒子認爸爸啊//我的新爸爸就是我原來的爺爺,我原來的爺爺?shù)男碌膬鹤泳褪俏襱ree[pos].up=upup;if(upup) tree[upup].c[tree[upup].c[1]==up]=pos;//當(dāng)然如果爺爺是虛空(原來的爸爸就是根節(jié)點)的話,就不能爸爸認兒子了//還有還有,因為父子關(guān)系發(fā)生了說不清道不明的改變,所以這里不好用which,要用which一開始定義的時候的用 }伸展 splay
我們發(fā)現(xiàn)通過rotate我們能在不改變樹的平衡性的同時讓某個點上升一層,但是這離我們的目標(biāo)(旋轉(zhuǎn)到根節(jié)點)還差得遠。
所以就有了splay操作:讓某個結(jié)點通過一次又一次rotate轉(zhuǎn)到根節(jié)點。比方說:
逆流而上的你眼前或許有無數(shù)曲折崎嶇道路,也許離終點遙遙無期,但是,
結(jié)點到達根源葉子結(jié)點\small 結(jié)點~~~~~~~~~~~到達根源~~~~~~~~~~~~~~~~~~~~~葉子結(jié)點結(jié)點???????????到達根源?????????????????????葉子結(jié)點
人一定要有夢想,沒有夢想和咸魚有什么區(qū)別?!~人一定要有夢想,沒有夢想和咸魚有什么區(qū)別?!?人一定要有夢想,沒有夢想和咸魚有什么區(qū)別?!
(這就是你四暗刻一向聽的時候碰碰杠杠做對對還一張dora沒有翻出來的原因?)
所以誰能告訴我這個“逆流而上的你”是什么啊還消不掉!(見下圖)
咳咳,話說回來,逆流而上的你眼前或許有無數(shù)曲折崎嶇道路,也許離終點遙遙無期,但是,絕無無法行走的路 (定義如此,走不了就不叫路了) ,只要你想要到達,就沒有無法克服的障礙,只要你想辦法的話。
在你一步一步往上rotate的時候,你的道路大概可以分為以下三類,六種:
還有一類沒有畫上去,就是爸爸就是根節(jié)點,沒有爺爺的情況,這個直接一個rotate就解決了。
折線形沒有什么好說的,一步一步rotate上去吧。
關(guān)于直線型:就是我是爸爸的a兒子,我爸爸是我爺爺?shù)腶兒子的情況。
科學(xué)家們告訴我們,這個時候應(yīng)該先rotate(爸爸),再rotate(我)。
如果仍然是一直埋頭苦干rotate(我),這樣的自平衡方法叫做Spaly;而先rotate(父親),再rotate(我)的自平衡方法叫做splay。
如下圖:
乍一看差不多,甚至某道題目(BZOJ1036樹的統(tǒng)計Count)我把splay換成了spaly會快一些(5600ms->5000ms),但是咨詢了Freopen/Kyle/wk大佬后,Freopen/Kyle/wk告訴我,“可以證明splay更優(yōu),而且出數(shù)據(jù)的時候可以卡spaly。”
(所以說科學(xué)家等于wk?)
哇。
總結(jié)一下:
| 我爸爸是根,我沒有爺爺 | rotate(我) |
| 我,我爸爸,我爺爺呈一條直線 | rotate(父),rotate(我) |
| 我,我爸爸,我爺爺呈一條折線 | rotate(我),rotate(我) |
所以就可以得到splay函數(shù):
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樹的根 }當(dāng)然,用splay操作可以使一個節(jié)點上升到它上面的任意一個結(jié)點
插入 insert
和正常的二叉平衡樹一樣,先找到對應(yīng)的位置,直接插入,沒問題的。
然后再spaly到根節(jié)點上去。
前驅(qū)&后繼 pre&nxt
也有叫upper和lower的,還有等等名字。
和一般的二叉平衡樹沒有什么區(qū)別。
求數(shù)的排名和排名上的數(shù)
我稱之為:getrank()getrank()getrank()和rankget()rankget()rankget()。
如果采用惰性方法的話就很方便。
現(xiàn)在我們給每個節(jié)點新增兩個變量,cnt和siz。
cnt代表這個點上重復(fù)有多少個數(shù)。比方說val=1的點的cnt=4,就代表插入了4個1,都被塞到同一個點里。
siz代表子樹里數(shù)的個數(shù)(也就是說,不是子樹點的個數(shù),而是子樹里各個點的cnt的值的和)
這樣子的話,插入和刪除會有些許變化。記得在樹的結(jié)構(gòu)或者點的cnt改變的時候pushup一下來維護siz。(也就是insert和delete和splay的時候→也就是splay的時候)到時候給出普通平衡樹模板的時候一并看吧。
然后rankget,就是和找前驅(qū)差不多了,用找前驅(qū)的方法加上siz這個變量就可以輕松把前驅(qū)是誰轉(zhuǎn)換成求前驅(qū)有幾個的問題了。就是找到前驅(qū)然后把前驅(qū)的siz+1就是答案了。
然后是getrank,感覺像find和getrank的結(jié)合體,有了siz和cnt,我們就知道每個pos的val所對應(yīng)的排名的區(qū)間是多少了。就是[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],如果在這個范圍里,那么就找到了,如果給定的排名在這個排名區(qū)間的左邊,那就說明我們要找的數(shù)比當(dāng)前的數(shù)要小,那么就向左二分下去,如果在右邊就向右邊。
需要稍微注意一下的是,向右邊搜的時候,給定的排名要減去(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),因為當(dāng)前節(jié)點的siz是它的左兒子+右兒子+自己,比方說左兒子和右兒子是[1,2,3,4,5,6,7]和[8,9,10]找第八名,當(dāng)然應(yīng)該在右兒子里面找了,但是右兒子只有3個數(shù)沒有第8名,所以把8-7(左兒子的siz)得到1,我們應(yīng)該在右兒子里面查詢第1大的數(shù)。
刪除 deleted
因為delete這個函數(shù)名已經(jīng)有了,所以加了一個d。
采用惰性刪除。
現(xiàn)在要分好幾種情況來討論。
首先,如果cnt-1之后還有剩余,就平安無事什么也不用干,cnt–就是了。
如果cnt==1,也就是刪除了這個點就沒有了:
(說實話空留一個cnt=0的點在那里浪費時間空間好像沒什么問題啊)
先把這個要刪除的東西splay到根節(jié)點處。
如果要刪除的點(目前已經(jīng)splay到根了),沒有兒子: 這棵樹的最后的一個數(shù)被你刪了,這棵樹完了。
如果只有一個兒子:那么就直接把這個根節(jié)點移除掉,并把根節(jié)點的位置傳給那個兒子。
如果有兩個兒子的話:
把前驅(qū)找到并splay上來,然后把被刪除點的右兒子接到前驅(qū)的右邊,自己消失掉。
現(xiàn)在普通平衡樹的各個功能就寫好了。
然后是,
合并 join
有一顆Splay樹(記為S1)的所有節(jié)點都比另一顆Splay樹(記為S2)的最小的節(jié)點小的時候,
于是讓S1最大的節(jié)點Splay到S1的根,然后把S2接到S1的右下方。
好雞肋的功能。
圖來自楊思雨的論文。
分離 split
給定數(shù)x,把一顆splay樹分成兩棵樹,其中一棵樹的值都小于x,另一顆都大于x。
首先把x這個點splay到根,然后它的左子樹和右子樹即為所求。
求最值 min&max
這個就一直往左or右走就是了。
翻轉(zhuǎn) turn
現(xiàn)在來考慮做文藝平衡樹。
文藝平衡樹要我們支持對一個數(shù)列進行區(qū)間翻轉(zhuǎn)再輸出。
首先,為了把用一棵樹來存一個數(shù)列,所以和普通的SBT不同(普通的SBT的中序遍歷是一個不下降序列)的,現(xiàn)在我們維護的splay樹的中序遍歷是這個區(qū)間本身。也就是從按權(quán)值不下降排序到下標(biāo)不下降排序。
舉個例子就是一個數(shù)組{1,3,4,5,6,7,2,4,5,2},在一個普通的Splay樹中,它的中序遍歷是{1,2,2,3,4,4,5,5,6,7},在支持區(qū)間翻轉(zhuǎn)的Splay樹中,中序遍歷是**{1,3,4,5,6,7,2,4,5,2}**。
然后怎么區(qū)間翻轉(zhuǎn)呢?
先建一顆樹,按照題目所要求的,就假設(shè)N=12,那么數(shù)列就是{1,2,3,4,5,6,7,8,9,10,11,12},建成splay樹以后可以長這個樣子:
上圖的確是跑splay的時候跑出來的。
現(xiàn)在我們可以看到,中序遍歷就是{1,2,3,4,5,6,7,8,9,10,11,12},假設(shè)現(xiàn)在我們要翻轉(zhuǎn)區(qū)間[l,r],比方說[4,6],就是圖中綠點的區(qū)間:
我們先想辦法把這個區(qū)間給獨立出來。
那么我們先把r+1這個點Splay到根節(jié)點,也就是Splay(7)。
轉(zhuǎn)上來了。
然后再把l-1轉(zhuǎn)到r+1的左兒子,也就是Splay(3,tree[root].c[0])。
畢竟上文說了,可以把一個點splay到它上方任意一個節(jié)點,而它肯定在根節(jié)點的左側(cè),那么根節(jié)點的左兒子一定在它的上方。
現(xiàn)在我們就把要操作的區(qū)間獨立出來了,就是根的左兒子的右子樹。(是一顆樹)
那么現(xiàn)在就可以做很多事情了。
比方說翻轉(zhuǎn),對于這個獨立出來的子樹,要翻轉(zhuǎn)相當(dāng)于交換每個節(jié)點的左右兒子,但是來如果要交換的話,那么就會很麻煩,況且一個區(qū)間被多次翻轉(zhuǎn)之后,很有可能翻轉(zhuǎn)回來,就浪費很多時間空間。
所以打懶標(biāo)記吧。
標(biāo)記一下這個點是否要翻轉(zhuǎn)左右兒子,輸出的時候如果有標(biāo)記就翻轉(zhuǎn)地輸出。
然后每次翻轉(zhuǎn)區(qū)間的時候不需要對整個區(qū)間打標(biāo)記,只需要在最上面的那個點那里打標(biāo)記就行了。
如果要訪問這個區(qū)間里沒有打過標(biāo)記的點,那么必然會訪問剛才打過標(biāo)記的那個“最上面那個點”,那么在訪問那個點的時候就把標(biāo)記下傳給兒子們,接下來訪問某個兒子,訪問這個兒子的時候再下傳給它的兒子……直到我們訪問到要找的那個點,此時它已經(jīng)得到懶標(biāo)記了,而整個過程幾乎沒有浪費時間在給暫時無關(guān)的結(jié)點打標(biāo)記上。
代碼就丟在文藝平衡樹里面吧。
對了對了,因為要訪問l?1\bold {l-1}l?1和r+1\bold {r+1}r+1這兩個結(jié)點,所以為了不在翻轉(zhuǎn)區(qū)間[1,x]\bold {[1,x]}[1,x]或[x,n]\bold {[x,n]}[x,n]的時候爆掉,要在1號點之前加一個-inf,在n號點之后加一個inf,既然這樣那么哪個點對應(yīng)哪個值就一定要想清楚了。
其他區(qū)間操作(以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.
翻譯
寫一個數(shù)據(jù)結(jié)構(gòu)支持六種操作:
①ADDxyDADD~ x ~y~ DADD?x?y?D,對于區(qū)間[x,y][x,y][x,y]每個數(shù)都加上DDD。
②REVERSExyREVERSE ~x~ yREVERSE?x?y,翻轉(zhuǎn)區(qū)間[x,y][x,y][x,y]。
③REVOLVExyTREVOLVE ~x ~y ~TREVOLVE?x?y?T,這個厲害了,把區(qū)間[x,y][x,y][x,y]里的每個數(shù)在這個區(qū)間里面循環(huán)右移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,求區(qū)間[x,y][x,y][x,y]的最小值。
一個個來
對于ADD操作,先把這個區(qū)間獨立出來,然后打一個加法懶標(biāo)記。
對于REVERSE操作,上面有。
對于REVOLVE操作,聲勢浩大,徒有其表,首先先把T%=(y-x+1);,那么就是把這個區(qū)間的后T個數(shù)移到前面y-x+1-T個數(shù)的前面;那么就是把前y-x+1-T個數(shù)REVERSE,把后T個數(shù)REVERSE,然后再把整個區(qū)間REVERSE就行了。
(這個字體的6寫得像4)
對于INSERT操作,因為它要求在某個點后面插入值,所以先把這個值xxx當(dāng)成一個區(qū)間[x,x][x,x][x,x](數(shù)學(xué)考試這么寫是會被扣分的)把它獨立出來,也就是先把x+1x+1x+1 SplaySplaySplay到根節(jié)點,再把x?1x-1x?1 SplaySplaySplay到根節(jié)點的兒子,那么xxx就在x?1x-1x?1的右兒子,然后再把PPP接上去就是了。
對于DELETE操作,和INSERT一樣,把xxx獨立出來以后直接取下來就是了。
對于MIN操作,因為我們的樹不是按照數(shù)值大小關(guān)系來排序的,所以要額外開一個值來記錄子樹里的最小值,和siz一起push_up。
代碼
普通平衡樹
咕咕咕
文藝平衡樹
咕咕咕
SuperMemo
咕咕咕
Splay的優(yōu)缺點
相較于AVL和Treap,Splay可以少存一個平衡因子。
Splay還有一個很重要的特性,那就是不穩(wěn)定性,可能飚的很快,也可能被神秘卡掉。
所以“在嚴謹場合”不建議使用。
代碼實現(xiàn)比AVL要簡單一些。
參考文章
以下每一篇都比我的這個好:
伸展樹的基本操作與應(yīng)用 IOI2004國家集訓(xùn)隊 楊思雨
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(時間復(fù)雜度分析)
https://blog.csdn.net/CABI_ZGX/article/details/82819882 (SuperMemo)
https://blog.csdn.net/DERITt/article/details/50485008 (更多的區(qū)間操作)
總結(jié)
以上是生活随笔為你收集整理的【原创】/Restarting/ Splay树 (普通平衡树 文艺平衡树 bzoj1895 poj 2580 SuperMemo 题解)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mtk 6577 root
- 下一篇: Liu C-2021-1: Nontri