伸展樹(SPLAY)個人總結+模板 [平衡樹]【資料結構】【模板】
前言
最近3個月內,無論是現場賽還線上賽中SPLAY出現的概率大的驚人啊啊啊!!!
然而不會的我就GG了,同時發現大家都會SPLAY,,,,然後就學習了一波。
開始怎麼學都學不懂,直到看到一句話
想學好splay,只要把伸展和旋轉操作弄懂,就好了.
(而這兩個想要學會就是需要自己畫圖自己理解了)
於是茅塞頓開,有了本文,
本文重點是SPLAY維護序列的操作,而非SPLAY本身,這部分會說的比較粗略,二叉樹的部分更不會有說明,
菜(sha3)逼我也只是初學,如果有描述不當甚至錯誤的地方,歡迎指正
定義
伸展樹(Splay Tree),也叫分裂樹,是一種二叉排序樹,它能在O(log n)內完成插入、查詢和刪除操作。
同其他平衡樹一樣,都是在二叉排序樹的基礎上進行操作的,但不同於AVL需要記錄平衡資訊,也沒有紅黑樹實現上的難度.是一種綜合考量下很適合應用於資訊學競賽的平衡樹.
對於一個基本的SPLAY 我這樣定義
int ch[N][2]; //ch[][0] 表示左兒子 ch[][1] 表示右兒子
int f[N]; //節點的父親節點
int sz[N]; //當前節點給所在的子樹的節點個數
int val[N]; //當前節點表示的值
int cnt[N]; //當前節點所表示的值的個數
int root; //記錄根節點的
int tot; //計算樹中節點個數
構建的過程也就和普通二叉樹一樣了,遞迴下去即可
void newnode(int rt,int v,int fa){
f[rt]=fa;
val[rt]=v;sz[rt]=1;
ch[rt][0]=ch[rt][1]=0;
}
void delnode(int rt){
f[rt]=sz[rt]=val[rt]=0;
ch[rt][0]=ch[rt][1]=0;
}
void build(int &rt,int l,int r,int fa){
if(l>r) return ;
int m = r+l >> 1;
rt=m; newnode(rt,val[rt],fa);cnt[rt]=1 ;
build(ch[rt][0],l,m-1,rt);
build(ch[rt][1],m+1,r,rt);
pushup(rt);
}
void init(int n){
root=0;
f[0]=sz[0]=ch[0][0]=ch[0][1]=rev[0]=0;
build(root,1,n,0);
pushup(root);
}
旋轉
對於一顆二叉排序樹,根據序列的資訊很容易找到某一個值,只要不斷的向下搜尋下去即可,複雜度是O(樹高),
但是二叉樹最壞的情況下是會退化成一個單鏈的,這是後查詢的複雜度就是O(n)了,非常不可取
而在SPLAY中控制樹保持平衡需要的就是旋轉操作,是樹保持平衡,這樣複雜度就變成了均攤
單旋: 左旋(zag)&右旋(zig)
雙旋:
通過樹的旋轉來自我調整來保持平衡,就是基於這兩個操作左旋(zag)&右旋(zig),還有其延伸出的操作
下面來實現下旋轉操作
總之就是對每次旋轉節點間關係資訊發生改變的位置調整好就行
需要點耐心,不要調錯
void rotate(int x,int k){ // k = 0 左旋, k = 1 右旋
int y=f[x];int z=f[y];
pushdown(y),pushdown(x);
ch[y][!k]=ch[x][k];if(ch[x][k])f[ch[x][k]]=y;
f[x]=z;if(z)ch[z][ch[z][1]==y]=x;
f[y]=x;ch[x][k]=y;
pushup(y),pushup(x);
}
伸展
經過多次旋轉,將節點位置坐出調整的操作就是伸展了
來舉個栗子,對於一個退化為單鏈的樹進行旋轉
雙旋的寫法,比較穩定
void splay(int x,int goal){//將x旋轉到goal的下面
while(f[x] != goal){
if(f[f[x]] == goal) rotate(x , ch[f[x]][0] == x);
else {
int y=f[x],z=f[y];
int K = (ch[z][0]==y);
if(ch[y][K] == x) rotate(x,!K),rotate(x,K);
else rotate(y,K),rotate(x,K);
}
}
pushup(x);
if(goal==0) root=x;
}
而我發現zig-zag這種兩個旋轉合在一起的操作,其實是兩遍單旋,所以只要每次都向上單旋就行了,
單旋容易被卡 不懂 百度伸展樹單雙旋的比較
void splay(int x,int goal){ //將x調整為goal的兒子,(如果要調整到根goal就是0)
for(int y=f[x];f[x]!=goal;y=f[x])
rotate(x,(ch[y][0]==x)); //(ch[y][0]==x)計算是左旋還是右旋,看x是左右兒子哪一個區分開了
if(goal==0) root=x;
}
各種操作
對於SPALY能夠做到的操作以【BZOJ3224 普通平衡樹】為引,以 【BZOJ 1895 & POJ 3580 supermemo 】做補充,如果不完全,後期會補上
一些基本操作
查詢
查詢部分和普通的二叉查詢樹一模一樣,只要遍歷下去即可
int search(int rt,int x){
if(ch[rt][0]&&val[rt]>x) return search(ch[rt][0],x);
else if(ch[rt][1]&&val[rt]<x)return search(ch[rt][1],x);
else return rt;
}
極值 & 前驅,後繼
前驅:小於x的最大的數
後繼:大於x的最小的數
先找到x所在的節點,然後在左右子樹,找最右左的節點即可
//以x為根的子樹 的極值點 0 極小 1 極大
int extreme(int x,int k){
while(ch[x][k])x=ch[x][k];splay(x,0);
return x;
}
第K個數
第k個數,通過記錄的sz[],很容易得到每個節點是第幾個,不斷的在樹上二分就行,
//以x為根的子樹 第k個數的位置
int kth(int x,int k){
if(sz[ch[x][0]]+1==k&&k<=sz[ch[x][0]]+cnt[x]) return x;
else if(sz[ch[x][0]]>=k) return kth(ch[x][0],k);
else return kth(ch[x][1],k-sz[ch[x][0]]-cnt[x]);
}
一些正經的操作
插入
假如要插入的點已經存在了,那麼cnt++就行了,
假如要插入的點在x
那麼讓x-1做為樹根,x+1伸展到根節點下面,那麼x+1的左兒子就是空出來的 加個值就好了
void _insert(int x){
int y=search(root,x),k=-1;
if(val[y]==x){
cnt[y]++;
sz[y]++;
for(int yy=y;yy;yy=f[yy]) pushup(yy);
}
else {
int p=prec(x),s=sufc(x);
splay(p,0);splay(s,p);
newnode(++tot,x,ch[root][1]);
ch[ch[root][1]][0]=tot;
for(int z=ch[root][1];z;z=f[z])pushup(z);
}
if(k==-1) splay(y,0);else splay(tot,0);
}
刪除
和刪除一樣,就是反過來了而已,
首先這個值如果不存在,那就直接return即可
如果這個值大於1,那就cnt–即可
如果這個值所在的節點的兒子節點有空的,那麼就把需要提上去的兒子節點提上去即可
如果這個值所在的節點的兒子節點都存在,那麼就把這個節點的前驅提到根節點,後繼提到根節點的下面,那樣的話刪除ch[ch[root][1]][0]就行了
void _delete(int x){
int y=search(root,x);
if(val[y]!=x) return;
if(cnt[y]>1){
cnt[y]--;
sz[y]--;
for(int yy=y;yy;yy=f[yy]) pushup(yy);
}
else if(ch[y][0]==0||ch[y][1]==0){
int z=f[y];
ch[z][ch[z][1]==y]=ch[y][ch[y][0]==0];
f[ch[y][ch[y][0]==0]]=z;delnode(y);
for(int yy=z;yy;yy=f[yy]) pushup(yy);
}
else {
int p=prec(x),s=sufc(x);
splay(p,0);splay(s,p);
ch[ch[root][1]][0]=0;
delnode(ch[ch[root][1]][0]);
for(int yy=s;yy;yy=f[yy]) pushup(yy);
}
}
區間加
區間操作
對於區間[l,r]
那麼讓l-1做為樹根,r+1伸展到根節點下面,那麼r+1的左兒子就是這個區間
打上lazy_tag就行了但為了更好的處理[1,n]這個區間 加上個0和n+1這兩個節點
//區間加
void add(int l,int r,int v){
int x=kth(root,l-1),y=kth(root,r+1);
splay(x,0);splay(y,x);
update_add(ch[y][0],v);
}
區間翻轉
同樣在一個二叉樹中 翻轉也就是讓每個節點的兩個兒子交換一下順序就好了,, 打個標記 就行了,
//區間翻轉
void reversal(int l,int r){
int x=kth(root,l-1),y=kth(root,r+1);
splay(x,0);splay(y,x);
update_rev(ch[y][0]);
}
區間交換
所以我們可以將後一個區間處理到一個子樹上,然後放到l−1,l 這兩個節點之間,就好了,先減掉,然後在加上去就好了
//區間交換
void exchange(int l1,int r1,int l2,int r2){
int x=kth(root,l2-1),y=kth(root,r2+1);
splay(x,0),splay(y,x);
int tmp_right = ch[y][0]; ch[y][0]=0;
x=kth(root,l1-1),y=kth(root,l1);
splay(x,0),splay(y,x);
ch[y][0] = tmp_right;
f[tmp_right]=y;
}
合併
合併是指兩顆SPLAY進行合併,
要求這兩顆樹沒有交錯的部分,(可能沒有這個限制,但是我不會,)
首先處理好一個樹A加入到B中,那麼在B中騰出一個空節點來代替A樹需要的段,然後把A樹的樹根放到呢個騰出的空節點位置就行了,
總結
SPLAY操作非常靈活多變,一定要理解SPLAY然後去使用,不要只會套板子就結束了,
有幾點特別要注意的地方
1.插入/刪除節點的時候注意父節點要修改
2.sz[]維護不要出錯
3. ……
——————————————————————————-
附上整體程式碼-md貼上來太卡了,去題解裡看吧
注:這兩個板子沒有用雙旋,用的單旋。。
相關推薦
伸展樹(SPLAY)個人總結+模板 [平衡樹]【資料結構】【模板】
前言 最近3個月內,無論是現場賽還線上賽中SPLAY出現的概率大的驚人啊啊啊!!! 然而不會的我就GG了,同時發現大家都會SPLAY,,,,然後就學習了一波。 開始怎麼學都學不懂,直到看到一句話 想學好splay,只要把伸展和旋轉操作弄懂,就好
【資料結構週週練】016 利用遞迴演算法及孩子兄弟表示法建立樹、遍歷樹並求樹的深度
一、前言 從今天起,就給大家分享一些樹的程式碼啦,不僅僅是二叉樹,我們要弄明白,普通的樹用資料結構怎麼儲存,它有哪些操作,它可以實現哪些功能? 可能大家要問了,二叉樹不是還沒有寫完嗎,線索二叉樹呢?二叉排序樹呢?平衡二叉樹呢?大家不要急,我們通過二叉樹來入門樹的演算法及程式碼實現,然後學
【資料結構週週練】015 利用遞迴演算法建立鏈式儲存的二叉樹並轉換左右孩子結點
一、前言 哈哈,今天就是程式設計師節啦,祝大家1024程式設計師節快樂。 今天要給大家分享的演算法是交換二叉樹是的左右孩子結點,比較簡單,需要建立一個結點用來暫存左孩子結點,下面給大家送上程式碼。 二、題目 將下圖用二叉樹存入,並交換二叉樹是的左右孩子結點。其中圓角矩形內為結點資
【資料結構週週練】014 利用棧和非遞迴演算法求鏈式儲存的二叉樹是否為完全二叉樹
一、前言 首先,明天是個很重要的節日,以後我也會過這個節日,在這裡,提前祝所有程式猿們,猿猴節快樂,哦不,是1024程式設計師節快樂。 今天要給大家分享的演算法是判斷二叉樹是否為完全二叉樹,相信大家對完全二叉樹的概念並不陌生,如果是順序儲存就會很方便,那鏈式儲存怎麼判斷呢,我的做法是:若
【資料結構週週練】013 利用棧和非遞迴演算法求二叉樹的高
一、前言 二叉樹的高是樹比較重要的一個概念,指的是樹中結點的最大層數本次演算法通過非遞迴演算法來求得樹的高度,借用棧來實現樹中結點的儲存。 學英語真的很重要,所以文中的註釋還有輸出以後會盡量用英語寫,文中出現的英語語法或者單詞使用錯誤,還希望各位英語大神能不吝賜教。 二、題目 將
【資料結構週週練】012 利用佇列和非遞迴演算法實現二叉樹的層次遍歷
一、前言 二叉樹的遍歷是比較多樣化的遍歷,有很多種遍歷方式,先序遍歷,中序遍歷,後序遍歷,層次遍歷等等。本次給大家講的是層次遍歷,為了方便,我將題目中的資料改為編號,從左往右,從上往下依次遍歷。方便大家看到結果。 二、題目 將下圖用二叉樹存入,並通過層次遍歷方式,自上而下,從左往右對
【資料結構週週練】010 遞迴演算法實現二叉樹的建立與遍歷
一、前言 上兩篇週週練部落格講了二叉樹的建立與遍歷,建立時,通過建立棧來存放結點,方便二叉樹的建立,這種建立二叉樹的方式採用了非遞迴演算法,本次內容採用遞迴的方式來建立二叉樹,大家可以通過對比程式碼量,感受一下遞迴的魅力。同時遍歷過程也是通過遞迴演算法。 如果大家第一次看
【資料結構週週練】020 利用遞迴判斷一棵二叉樹是否為二叉排序樹
一、二叉排序樹 二叉排序樹可以說是資料結構中相當重要的內容之一啦,前兩次給大家講了二叉排序樹的建立、遍歷與查詢。今天給大家分享的是二叉排序樹的應用,判斷一個二叉樹是否為一棵二叉排序樹。 二叉排序樹的特點大家都知道,左子樹根結點值<根結點<右子樹根結點值,並且中
【資料結構週週練】022 從大到小輸出二叉排序樹中小於某個值的所有結點編號及資料
一、二叉排序樹 今天給大家分享的是二叉排序樹的應用,從大到小輸出二叉排序樹中小於某個值的所有結點編號及資料。 我們知道,我們做中序遍歷時,先訪問左子樹,再訪問根節點,最後訪問右子樹;通過中序遍歷會得到一個遞增的序列。該應用要求得到從大到小,一個遞減的序列,我們可以通過先訪
【資料結構和演算法06】2-3-4樹
從第4節的分析中可以看出,二叉搜尋樹是個很好的資料結構,可以快速地找到一個給定關鍵字的資料項,並且可以快速地插入和刪除資料項。但是二叉搜尋樹有個很麻煩的問題,如果樹中插入的是隨機資料,則執行效果很好,但如果插入的是有序或者逆序的資料,那麼二叉搜尋樹的執行速度就變得很慢
【資料結構週週練】029 判斷無向圖是否為一棵樹演算法原理詳解及程式碼分享
一、題目 設計一個演算法,判斷一個圖G是否為一棵樹,如果是,返回TRUE,否則,返回FALSE。 二、美麗的星座 星座真的好美好美。特別是當人類給它們賦予含義的那一刻,更美,彷彿有了靈魂一般。 是不是很美,是不是?你以為我是讓你過來看星星的嗎?你以為我是
[模板]平衡樹splay
氣死我了,調了一個下午+兩節課,各種大大小小的錯誤,各種調QAQ,最後總之是調出來了. 其實就是一個雙旋操作,然後其他就是左兒子<當前節點<右兒子,剩下就是細節了. 題幹: 題目描述 您需要寫一種資料結構(可參考題目標題),來維護一些數,其中需要提供以下操作: 插入
[Splay]luogu P3391 文藝平衡樹
mes hide 我們 ++ else closed src n+1 clas 題目描述 https://www.luogu.org/problemnew/show/P3391 分析 文藝平衡樹 一道大家熟知的splay區間翻轉模板題 基於splay的區間翻轉,我們
二叉樹入門個人總結
二叉樹是pat高頻考點,最近做了幾道入了個門,簡單總結下。(水平有限,大神繞道) 一、二叉樹 最典型的就是已知前序中序建樹或已知後序中序建樹,接著再後序或前序或層序遍歷。難點主要在於建樹。代表題目
luogu P3765 總統選舉(線段樹維護摩爾投票+平衡樹)
這題需要一個黑科技——摩爾投票。這是一個什麼東西?一個神奇的方法求一個序列中出現次數大於長度一半的數。 簡而言之就是同加異減; 比如有一個代表投票結果的序列。 \[[1,2,1,1,2,1,1]\] 我們記錄一個\(num\)和\(cnt\)先別管它們是幹什麼的。我們模擬一遍模擬排序。 \[首先讀第一個數1,
【資料結構】【平衡樹】淺析樹堆Treap
【Treap】 【Treap淺析】 Treap作為二叉排序樹處理演算法之一,首先得清楚二叉排序樹是什麼。對於一棵樹的任意一節點,若該節點的左子樹的所有節點的關鍵字都小於該節點的關鍵字,且該節點的柚子樹的所有節點的關鍵字都大於該節點的關鍵字,則這棵樹是一棵二叉排序樹。 Treap在每一個節點中有兩個最
【資料結構與演算法】Size Balanced Tree(SBT)平衡二叉樹
Size Balanced Tree(SBT)平衡二叉樹 定義資料結構 struct SBT { int key,left,right,size; } tree[N]; key:儲存值,left,right:左右子樹,size:保持平衡最終要的資料,表示子
【資料結構】平衡搜尋樹之---B樹的演算法實現
#include<iostream> using namespace std; #ifndef __BTREE_H__ #define __BTREE_H__ template<class K,int M=3>//設為三階B樹(每個陣列三個關鍵字
【資料結構】平衡二叉樹的構建以及增加刪除操作
一、前言 最近學習中遇到了平衡二叉樹的實用,要求是對一個數據列,進行平衡二叉樹的排列,並畫出結果,小編剛開始的時候不是很會,通過總結資料學習了一下平衡二叉樹的相關知識,通過部落格總結一下。
平衡二叉樹的C語言實現(建立、插入、查詢、刪除、旋轉)【資料結構】
平衡二叉樹(AVL)或者是一顆空樹,或者是具有下列性質的非空二叉搜尋樹: (1). 任一結點的左、右子樹均為AVL樹; (2). 任一結點的左、右子樹高度差的絕對值不超過1。 對於二叉樹中任一結點T,其“平衡因子”(Balance Factor, BF)定義為BF(T)