簡介
平衡二叉樹(Balanced Binary Tree)具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹(摘自百度百科)。
splay又名Splay Balanced Tree(SBT),通過雙旋來維持它平衡樹的性質.
同時有類似的結構Spaly 我也不知道是不是真的有 , 只用單選來維護平衡樹.
struct node{
int fa;//記錄節點父親
int ch[2];//ch[0]表示左兒子,ch[1]表示右兒子
int val;//記錄節點權值
int size;//記錄節點子數大小(包括該節點)
int cnt;//記錄同樣權值的元素個數
int mark;//記錄反轉區間標記(普通平衡樹不用)
}t[N];
另外補充說明一下size在記錄子樹大小的時候指的是以node為根的整顆子樹的元素個數,而不是節點個數(有相同權值的時候都要統計進來).
splay具有這樣的性質:
- 一個節點的權值總比它的左兒子大,比它的右兒子小.
- splay樹的中序遍歷結果就是該序列從小到大排列.
下面先介紹一下旋轉操作.
旋轉首先需要查找一個節點屬於左節點還是右節點.
bool get(int x){
return t[t[x].fa].ch[1] == x;//是右兒子返回1,左兒子返回0
}
並且在將一個節點向上旋的過程中因為節點的關系發生了變化,所以需要重新統計.
void up(int x){
t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;
}

假設現在要將x右旋到fa的位置(向哪個方向旋就叫什么旋),那么步驟如下:
先找出x屬於哪邊兒子(左兒子還是右兒子):d1(d1為0表示x是左兒子,為1表示是右兒子).圖中d1==0(x是左兒子).然后斷開x與 t[x].ch[d1^1] 的連邊,並將 t[x].ch[d1^1] 連到fa上代替x的位置.

然后斷開father與grandfather的連邊,將x接上去代替father的位置,並將father以及它整顆子樹向下拉.

最后再把father與x連邊,一次旋轉就完成了.

void rotate(int x){
int fa = t[x].fa , gfa = t[fa].fa;
int d1 = get(x) , d2 = get(fa);
t[fa].ch[d1] = t[x].ch[d1^1] ; t[t[x].ch[d1^1]].fa = fa;
t[gfa].ch[d2] = x; t[x].fa = gfa;
t[fa].fa = x; t[x].ch[d1^1] = fa;
up(fa); up(x);//up是收集節點子樹的個數
}
這樣旋轉后並沒有改變它二叉平衡樹的性質.並且雙旋操作可以減小平衡樹的期望深度. (至於為什么可以自己出一組稍微大一點的數據模擬一下)
雙旋操作指的是當要旋轉的節點與它父親在同一邊時(它和父親都是左兒子或右兒子),先旋轉父親,再旋轉它自己.
play操作其實就是模擬的一個節點向上轉的過程,下面直接看注釋:
void splay(int x,int goal){//goal是將x旋轉到goal的下面
while(t[x].fa != goal){
int fa = t[x].fa , gfa = t[fa].fa;
int d1 = get(x) , d2 = get(fa);
if(gfa != goal){
if(d1 == d2) rotate(fa);//若在同邊,則先轉father(雙旋)
else rotate(x);//否則直接將x向上旋
}
rotate(x);//再向上旋一次
}
if(goal == 0) root = x;//用goal==0來表示將x轉到根節點
}
下面看一下splay過程的圖解(splay(x,goal)):

(原圖)

(x與father同側,先轉father)

(再轉x)

(最后將x旋轉到goal的下面).
通過上面這幾個函數,我們已經可以維護splay它平衡樹的性質了,然后splay還有一些操作:
- 插入一個數字.
- 刪除一個數字.
- 查詢一個數字的排名.
- 查找一個數字的前驅/后繼.
- 查詢第k小的數字是多少.
- 查詢最值.
首先我們來看如何插入一個數字.
插入節點時是按照新插入的節點權值來遍歷splay找到它應該插入的位置的.所以就在遍歷splay時記錄一下父親,直到找到應該插入的位置就加新節點.
void insert(int val){
int node = root , fa = 0;
while(node && t[node].val != val)
fa = node , node = t[node].ch[t[node].val<val];
if(node) t[node].cnt++;//如果已經存在該權值的節點,則直接給該節點所含相同數字個數++
else{//否則新開一個編號存節點
node = ++cnt; if(fa) t[fa].ch[t[fa].val<val] = node;
t[node].fa = fa;
t[node].val = val;
t[node].cnt = 1;
t[node].size = 1;
}
splay(node , 0);//將新節點旋到根來維護splay的子樹
}
最后將新插入的節點旋到根,新插入節點后的splay樹就被維護好了.
查詢第k小的數字
我們已經知道了splay的二叉平衡樹的性質,並且通過splay操作維持了它平衡樹的性質,那么在查詢第k小的數字時,就可以直接比較k與節點size的大小來確定第k小的數字在哪個位置了.我們用node來表示當前遍歷到的節點(最開始從root出發).
- 如果k比node左子樹的size值要小的話,那么第k小一定在node的左子樹中.
- 如果k比node左子樹和node節點所含數字個數還要多,那么一定在右子樹中.
- 如果這兩個情況都不滿足,則node就是第k小.
int kth(int k){
int node = root;
while(1){
int son = t[node].ch[0];
if(k <= t[son].size) node = son;
else if(k > t[son].size+t[node].cnt){
k -= t[son].size + t[node].cnt;
node = t[node].ch[1];
}
else return t[node].val;
}
}
查詢一個數的排名
要查找一個數的排名,首先要找到它在splay樹中的位置.同樣也是通過權值來遍歷.
int find(int val){
int node = root;
while(t[node].val!=val && t[node].ch[t[node].val<val])
node = t[node].ch[t[node].val<val];
return node;
}
這樣找出來的編號就是權值為val的節點.若不存在這樣的節點,則會找到葉子節點(此時權值不一定最接近查詢的值,但是可以通過這樣來找樹中的最值).
找到要查的數字后,直接將它旋轉到根,此時它左子樹的size+1就是它的排名.
int get_rank(int val){
splay(find(val) , 0);
return t[t[root].ch[0]].size+1;
}
查找一個數的前驅/后繼
為了方便操作,可以先把要查找的值先旋到根.
此時如果要查詢前驅,前驅一定就是根節點或是在它的左子樹中最大的值.那么先比較根節點的權值與要查詢的值,如果要查詢前驅並且根節點的權值已經比要找前驅的權值要小了,那么根節點就是要查找的前驅.
為什么一定是這樣的呢?因為find找到一個結點要么找到的是該節點,要么就是與要找的權值最接近的節點.所以根節點的權值與查找的權值最接近.而前驅就是比它權值要小的最大的數,所以根節點就是前驅了.
如果根節點不是前驅,那么前驅就是它左子樹中的最大值(也就是左子樹最右邊的節點).
int get_pre(int val,int kind){//前驅后繼查詢寫在了同一個函數里,kind==0表示查找前驅,kind==0表示查找后繼
splay(find(val) , 0); int node = root;
if(t[node].val<val && kind == 0) return node;
if(t[node].val>val && kind == 1) return node;//根節點就是前驅/后繼的的情況
node = t[node].ch[kind];
while(t[node].ch[kind^1])
node = t[node].ch[kind^1];//否則找到根節點子樹中的最值
return node;
}
刪除一個數
刪除一個數,也是先要確定這個節點的位置.但是刪除一個節點不能直接將要刪除的節點旋轉到根.因為如果旋轉到根節點之后它有可能還有左右子樹.
所以我們可以先找到它的前驅后繼,然后將前驅旋到根,后繼旋到前驅的下面.此時要刪除的點就是后繼的左兒子.
因為前驅是第一個比它小的數字,所以它在前驅的右邊,后繼是第一個比它大的數字,所以他在后繼的左邊,后繼旋到了前驅的下面,那么要刪除的節點就一定在前驅后繼的中間,也就是后繼的左兒子.

然后找到它的位置進行刪除.
void delet(int val){
int last = get_pre(val,0);
int next = get_pre(val,1);
splay(last , 0); splay(next , last);
if(t[t[next].ch[0]].cnt > 1){
t[t[next].ch[0]].cnt--;
splay(t[next].ch[0],0);//同樣將未刪完的節點轉到根重新統計子樹大小
}
else t[next].ch[0] = 0;//如果能直接刪除,則直接去掉這條連邊
}
查詢最值
查詢最值也是通過find函數會找到與一個數最接近的節點的特性,直接find(inf)或者是find(-inf)來找與正無窮最接近的值(最大值)或與負無窮最接近的值(最小值).
到這里,splay的基本操作就講完了.下面是模板代碼.
普通平衡樹
#include<bits/stdc++.h>
#define b out(root),cout << endl;
using namespace std;
const int N=100000+5;
const int inf=2147483647;
int n;
int cnt = 0;
int root = 0;
struct splay{
int ch[2], size, cnt, val, fa;
}t[N];
int gi(){
int ans = 0 , f = 1; char i = getchar();
while(i<'0'||i>'9'){if(i=='-')f=-1;i=getchar();}
while(i>='0'&&i<='9'){ans=ans*10+i-'0';i=getchar();}
return ans * f;
}
void out(int x){
if(t[x].ch[0]) out(t[x].ch[0]);
printf("%d ",t[x].val);
if(t[x].ch[1]) out(t[x].ch[1]);
}
int get(int x){
return t[t[x].fa].ch[1] == x;
}
void up(int x){
t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;
}
void rotate(int x){
int fa = t[x].fa , gfa = t[fa].fa;
int d1 = get(x) , d2 = get(fa);
t[fa].ch[d1]=t[x].ch[d1^1] , t[t[x].ch[d1^1]].fa=fa;
t[gfa].ch[d2]=x , t[x].fa=gfa;
t[fa].fa=x , t[x].ch[d1^1]=fa;
up(fa); up(x);
}
void splay(int x,int goal){
while(t[x].fa != goal){
int fa = t[x].fa, gfa = t[fa].fa;
int d1 = get(x), d2 = get(fa);
if(gfa != goal){
if(d1 == d2) rotate(fa);
else rotate(x);
}
rotate(x);
}
if(goal == 0) root = x;
}
int find(int val){
int node = root;
while(t[node].val != val && t[node].ch[t[node].val<val])
node = t[node].ch[t[node].val<val];
return node;
}
void insert(int val){
int node = root, fa = 0;
while(t[node].val != val && node)
fa = node, node = t[node].ch[t[node].val<val];
if(node) t[node].cnt++;
else{
node = ++cnt;
if(fa) t[fa].ch[t[fa].val<val] = node;
t[node].size = t[node].cnt = 1;
t[node].fa = fa; t[node].val = val;
}
splay(node , 0);
}
int pre(int val,int kind){
splay(find(val) , 0); int node = root;
if(t[node].val < val && kind == 0) return node;
if(t[node].val > val && kind == 1) return node;
node = t[node].ch[kind];
while(t[node].ch[kind^1])
node = t[node].ch[kind^1];
return node;
}
void delet(int val){
int last = pre(val,0), next = pre(val,1);
splay(last , 0); splay(next , last);
if(t[t[next].ch[0]].cnt > 1){
t[t[next].ch[0]].cnt--;
splay(t[next].ch[0] , 0);
}
else t[next].ch[0] = 0;
}
int kth(int k){
int node = root;
if(t[node].size < k) return inf;
while(1){
int son = t[node].ch[0];
if(k <= t[son].size) node = son;
else if(k > t[son].size+t[node].cnt){
k -= t[son].size+t[node].cnt;
node = t[node].ch[1];
}
else return t[node].val;
}
}
int get_rank(int val){
splay(find(val) , 0);
return t[t[root].ch[0]].size;
}
int main(){
insert(-inf); insert(inf);
int flag, x; n = gi();
for(int i=1;i<=n;i++){
flag = gi(); x = gi();
if(flag == 1) insert(x);
if(flag == 2) delet(x);
if(flag == 3) printf("%d\n",get_rank(x));
if(flag == 4) printf("%d\n",kth(x+1));
if(flag == 5) printf("%d\n",t[pre(x,0)].val);
if(flag == 6) printf("%d\n",t[pre(x,1)].val);
}
return 0;
}
當然,這還不夠,splay還有一個強大的功能:翻轉區間
要找到一段區間,可以利用刪除數字的思想.先找到區間左端點的前驅旋轉到根,再找到區間右端點的后繼旋轉到前驅下面,此時要找的區間就能確定就是后繼的左子樹.然后再給節點打上標記,用線段樹的思想不斷處理標記,最后再查詢的時候再將標記下放,就可以維護出翻轉后的序列.
文藝平衡樹
#include<bits/stdc++.h>
using namespace std;
const int N=100000+5;
const int inf=2147483647;
int n, m;
int cnt = 0;
int root = 0;
struct node{
int ch[2], fa, size, mark, val;
}t[N];
bool get(int x){
return t[t[x].fa].ch[1] == x;
}
void up(int x){
t[x].size = t[t[x].ch[0]].size + t[t[x].ch[1]].size + 1;
}
void rotate(int x){
int fa = t[x].fa , gfa = t[fa].fa , d1 = get(x) , d2 = get(fa);
t[fa].ch[d1] = t[x].ch[d1^1]; t[t[x].ch[d1^1]].fa = fa;
t[gfa].ch[d2] = x; t[x].fa = gfa;
t[fa].fa = x; t[x].ch[d1^1] = fa;
up(fa); up(x);
}
void splay(int x,int goal){
while(t[x].fa != goal){
int fa = t[x].fa , gfa = t[fa].fa;
int d1 = get(x) , d2 = get(fa);
if(gfa != goal){
if(d1 == d2) rotate(fa);
else rotate(x);
}
rotate(x);
}
if(goal == 0) root = x;
//printf("root = %d\n",root);
}
void insert(int val){
int node = root , fa = 0;
while(node && t[node].val != val)
fa = node , node = t[node].ch[t[node].val<val];
node = ++cnt;
if(fa) t[fa].ch[t[fa].val<val] = node;
t[node].fa = fa;
t[node].val = val;
t[node].size = 1;
splay(node , 0);
}
void pushdown(int x){
t[t[x].ch[0]].mark ^= 1;
t[t[x].ch[1]].mark ^= 1;
t[x].mark = 0;
swap(t[x].ch[0] , t[x].ch[1]);
}
int kth(int k){
int node = root;
while(1){
if(t[node].mark) pushdown(node);
int son = t[node].ch[0];
if(k<=t[son].size) node = son;
else if(k>t[son].size+1){
k -= t[son].size+1;
node = t[node].ch[1];
}
else return node;
}
}
void work(int l,int r){
int left = kth(l) , right = kth(r);
splay(left , 0) ; splay(right , left);
t[t[t[root].ch[1]].ch[0]].mark ^= 1;
}
void output(int x){
if(t[x].mark) pushdown(x);
if(t[x].ch[0]) output(t[x].ch[0]);
if(t[x].val>=1 && t[x].val<=n) printf("%d ",t[x].val);
if(t[x].ch[1]) output(t[x].ch[1]);
}
int main(){
insert(inf); insert(-inf);
int x, y; cin >> n >> m;
for(int i=1;i<=n;i++) insert(i);
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y);
work(x , y+2);
}
output(root); cout << endl;
return 0;
}
明白了這幾個模板之后,就可以做點簡單的題目練練手了.
LIST
- [HNOI2002]營業額統計
- [NOI2004]郁悶的出納員
- [JSOI2008]最大數
- [NOI2003]文本編輯器
- [ZJOI2006]書架
- [HNOI2004]寵物收養場
- [HNOI2012]永無鄉
- [NOI2005]維護數列
題解
T1:
動態查詢前驅並統計答案,沒什么好講的吧.
T2:
對於修改所有人的工資,可以直接用變量保存所有人被修改的工資而不用一個個修改.然后在查詢的時候直接找到第一個比勸退標准低的人,刪除的時候直接把它以及它左邊的全部刪掉.也就是在刪除的過程中找到它以及它子樹的位置旋到根節點的右兒子的左兒子,然后直接刪除它的父指針.
T3:
插入的時候直接插入到樹的最右邊,這樣維護的一顆splay的中序遍歷結果就是這個序列了.然后在節點維護一個最大值,查找的時候就先找到它前面一個元素的排名旋轉到根,那么根節點的右兒子的最大值就是答案了.
T4:
按照題意模擬,可以在插入一個序列的時候先將這個序列處理成一棵樹然后再合並.
T5:
可以考慮給每個元素定義一個優先值來維護平衡樹的性質(反正當時我做這道題的時候老是搞不清,就這么寫了).用一個數組記錄一本書的編號映射到樹中的優先值.
- 對於要放到書架頂端的書,先將它刪除,然后再給它賦一個最小值插入樹中.
- 對於要放到書架底端的書同理.
- 對於要與前驅后繼交換的書,先交換編號映射的優先值,然后再分別刪除,插入這兩個點.
- 其他直接模板操作解決.
T6:
因為沒有領養者的時候來領養者,或是沒有寵物的時候來寵物,都會找到目前樹中與該值最接近的一個(前驅后繼中取min),所以考慮用一個計數器統計當前領養者/寵物數,來表示目前狀態的樹為寵物樹/領養者樹,然后再對這些情況分類討論一下就可以了.
T7:
鏈接
<\br>
T8:
按照題意模擬...注意細節,具體代碼實現可以戳這里
