替罪羊樹摘要

原理

替罪羊樹不依靠旋轉,而是依靠重構不平衡的子樹使整棵樹達到平衡狀態。php

變量介紹

 1 int n;  2 struct Node {  3     int val,num;                                                                    //節點的值,數量 
 4     int siz,hid;                                                                    //以該節點爲根的子樹中未被刪的節點數和已被刪的節點數 
 5     int son[2],fa;                                                                    //左(0)右(1)兒子,爸爸 
 6     char id;                                                                        //是否被刪(0否1是) 
 7     void res() {                                                                    //清空,用於重構 
 8         hid=fa=val=num=siz=son[0]=son[1]=id=0;  9  } 10 } tree[maxn]; 11 int ins,mem[maxn],inm,root;                                                            //新節點內存回收池 
12 int reb[maxn],ren[maxn],inr;                                                        //臨時數組,重構用 
var

各類操做

暴力重構

當一顆子樹不平衡時,便將其重構,這是替罪羊樹的核心思想。判斷一顆子樹是否平衡的方法有不少,我採用的方法是,若一顆子樹的某個兒子子樹的大小大於這顆子樹的大小的alpha倍,或這顆子樹被刪除的節點數超過這顆子樹總結點數的alpha倍,則認爲這顆子樹是不平衡的,須要重構。咱們也能夠知道,當alpha>=1或alpha<=0.5的時候無心義。alpha越大,重構次數越少,但這顆樹越不平衡;alpha越小,樹越平衡,但重構次數越多。所以,咱們折中取alpha=0.75。(因爲不想用浮點數,這裏改爲了整數乘法運算)html

1 char need_to_reset(int now) {                                                        //以當前節點爲根的子樹是否須要重構 
2     return tree[now].siz*3<tree[tree[now].son[0]].siz*4
3     ||tree[now].siz*3<tree[tree[now].son[1]].siz*4||tree[now].siz<tree[now].hid*3;    //若某顆子樹大小超過整棵子樹的3/4或 4                                                                                     //被刪除的節點超過總結點數的3/4則需重構 
5 }
need_to_reset

重構有兩種方法,我採用的是時空複雜度O(n)的中序遍歷法(ps:所謂拍扁重構法是用鏈表的時間O(n)空間O(logn)的方法,本蒟蒻不會)。將須要的子樹中序遍歷,獲得一個序列,再每次二分,取最中間的那個數建根,左右遞歸建樹。c++

 1 void redfs(int now) {
 2     if(tree[now].son[0]) redfs(tree[now].son[0]);
 3     if(!tree[now].id)reb[++inr]=tree[now].val,ren[inr]=tree[now].num;                //若沒被刪除則加入臨時數組,不然不加入 
 4     if(tree[now].son[1]) redfs(tree[now].son[1]);
 5     mem[++inm]=now;
 6     tree[now].res();                                                                //清空 
 7 }
 8 void rebuild(int l,int r,int pla,int f) {
 9     if(l>r)return;
10     if(l==r) {
11         tree[pla].val=reb[l];
12         tree[pla].siz=tree[pla].num=ren[l];
13         tree[pla].fa=f;
14         return;
15     }
16     int mid=l+r>>1;
17     tree[pla].val=reb[mid];
18     int s1,s2;
19     tree[pla].siz=tree[pla].num=ren[mid];
20     tree[pla].fa=f;
21     if(l<mid) {                                                                        //建左子樹 
22         s1=tree[pla].son[0]=mem[inm--];
23         rebuild(l,mid-1,s1,pla);
24     }
25     if(mid<r) {                                                                        //建右子樹 
26         s2=tree[pla].son[1]=mem[inm--];
27         rebuild(mid+1,r,s2,pla);
28     }
29     update(pla);
30 }
31 void reset(int rot) {
32     int f=tree[rot].fa;
33     inr=0;                                                                            //這個清零應該不會忘 
34     redfs(rot);
35     rebuild(1,inr,((root==rot)?root=mem[inm--]:mem[inm--]),f);                        //注意,若需重構整棵樹則要更新root 
36     for(;tree[rot].fa;rot=tree[rot].fa)update(tree[rot].fa);
37 }
reset

插入git

和普通的平衡樹同樣插入,注意要用遞歸,而且傳參的時候傳一個引用,用來存回溯時最淺的須要被重構的子樹根編號。數組

 1 void insert(int now,int x,int &ntr) {                                                //ntr爲引用,由於須要在回溯時找到最淺的替罪羊節點  2                                                                                     //注意必定要用遞歸式的,用循環式的就會像我同樣WA上天  3                                                                                     //刪除也是同樣 
 4     if(!now) {                                                                        //走到這個if裏面說明整棵樹爲空 
 5         int u=(inm?mem[inm--]:++ins);  6         tree[u].siz=tree[u].num=1;  7         tree[u].val=x;  8         root=u;  9         return; 10  } 11     if(tree[now].val==x) {                                                            //當前節點的值等於需插入的值 
12         if(tree[now].id) tree[now].id=0,tree[now].hid--;                            //若是被刪除則恢復 
13         tree[now].siz++,tree[now].num++; 14         if(need_to_reset(now)) ntr=now;                                                //隨手一判 
15         return; 16  } 17     if(tree[now].son[tree[now].val<x]) insert(tree[now].son[tree[now].val<x],x,ntr);//往正確的兒子走 
18     else {                                                                            //走到底了 
19         int u=(inm?mem[inm--]:++ins); 20         tree[u].siz=tree[u].num=1; 21         tree[u].fa=now; 22         tree[u].val=x; 23         tree[now].son[tree[now].val<x]=u; 24  } 25  update(now); 26     if(need_to_reset(now)) ntr=now; 27 }
insert

刪除

替罪羊樹的刪除並非真正意義上的刪除,而是在這個數被刪除消失時打上一個刪除標記,當重構時再真正刪除,或再次添加這個數來取消這個標記。(ps:這個思想在許多數據結構中都很是有用)數據結構

 1 void delet(int now,int x,int &ntr) {  2     if(tree[now].val==x) {  3         if(tree[now].num) tree[now].num--,tree[now].siz--,tree[now].hid++;  4         if(!tree[now].num) tree[now].id=1;  5         if(need_to_reset(now)&&(tree[now].son[0]||tree[now].son[1])) ntr=now;        //若是這個點是葉子節點就不判,不要問我爲何 
 6         return;  7  }  8     if(tree[now].son[tree[now].val<x]) delet(tree[now].son[tree[now].val<x],x,ntr);  9  update(now); 10     if(need_to_reset(now)) ntr=now; 11 }
delet

查詢x排名

和其餘平衡樹同樣查找就好了,無所謂遞歸或循環。ide

1 int findran(int x) { 2     int u=root,ans=1; 3     for(; u&&tree[u].val!=x; u=tree[u].son[x>tree[u].val])                            //不須要判重構,因此能夠用循環 
4         ans+=(x>tree[u].val)*(tree[tree[u].son[x<tree[u].val]].siz+tree[u].num); 5     if(u) ans+=tree[tree[u].son[0]].siz; 6     return ans; 7 }
finran

查詢排名爲x的數

和其餘平衡樹同樣查找就好了。函數

1 int findnum(int x) { 2     int u=root; 3     for(char t; tree[u].son[tree[tree[u].son[0]].siz+tree[u].num<x?1:0]&&
4         (tree[tree[u].son[0]].siz>=x||tree[tree[u].son[0]].siz+tree[u].num<x); 5         t=tree[tree[u].son[0]].siz+tree[u].num<x, 6         x-=t*(tree[tree[u].son[0]].siz+tree[u].num),u=tree[u].son[t]); 7     return tree[u].val; 8 }
finnum

查詢x前驅

先找到這個數的排名y,再找到排名爲y-1的數就是x的前驅。ui

1 int las(int x) { 2     int rnk=findran(x);                                                                //先找到這個點的排名 
3     return findnum(rnk-1);                                                            //再找到比它小的最大的數 
4 }
las

查詢x後繼

先找到這個數的最大排名(即這個數的排名+這個數的個數-1)y,再找到排名爲y+1的數就是x的後繼。spa

1 int nex(int x) { 2     int u=root,ans=0;                                                                //不要問我爲何這個寫得這麼醜 
3     for(; u&&tree[u].val!=x; u=tree[u].son[x>tree[u].val])                            //找到這個數的最大排名 
4         ans+=(x>tree[u].val)*(tree[tree[u].son[x<tree[u].val]].siz+tree[u].num); 5     if(u) ans+=tree[tree[u].son[0]].siz+tree[u].num; 6     return findnum(ans+1);                                                            //查詢比它大的最小的數 
7 }
nex

時空複雜度

時間複雜度

重構:單次重構的複雜度爲O(n),但經過勢能分析能夠得出重構的均攤複雜度爲O(logn)。推導過程來自VFleaking大佬的論文(Orzzzzzz)。

 

 

插入、刪除、查詢:因爲保證樹高均攤logn,所以複雜度爲均攤O(logn)

常數還能夠,彷佛僅比紅黑樹慢,但單一的alpha容易被卡。所以能夠開始就隨機若干個alpha,取出合格的取平均值

空間複雜度

O(n)

例題

洛谷P3369【模板】普通平衡樹

 1 #include<bits/stdc++.h>
 2 using namespace std;  3 #define INF 0x7fffffff
 4 #define ME 0x7f
 5 #define FO(s) freopen(s".in","r",stdin);freopen(s".out","w",stdout)
 6 #define fui(i,a,b,c) for(int i=(a);i<=(b);i+=(c))
 7 #define fdi(i,a,b,c) for(int i=(a);i>=(b);i-=(c))
 8 #define fel(i,a) for(register int i=h[a];i;i=ne[i])
 9 #define ll long long
10 #define MEM(a,b) memset(a,b,sizeof(a))
11 #define maxn (100000+10)
12 int n; 13 struct Node{int val,num;int siz,hid;int son[2],fa;char id;void res(){hid=fa=val=num=siz=son[0]=son[1]=id=0;}}tree[maxn]; 14 int ins,mem[maxn],inm,root; 15 int reb[maxn],ren[maxn],inr; 16 template<class T>
17 inline T read(T &n){ 18     n=0;int t=1;double x=10;char ch; 19     for(ch=getchar();!isdigit(ch)&&ch!='-';ch=getchar());(ch=='-')?t=-1:n=ch-'0'; 20     for(ch=getchar();isdigit(ch);ch=getchar()) n=n*10+ch-'0'; 21     if(ch=='.') for(ch=getchar();isdigit(ch);ch=getchar()) n+=(ch-'0')/x,x*=10; 22     return (n*=t); 23 }void update(int x){ 24     tree[x].siz=tree[tree[x].son[0]].siz+tree[tree[x].son[1]].siz+tree[x].num; 25     tree[x].hid=tree[tree[x].son[0]].hid+tree[tree[x].son[1]].hid+tree[x].id; 26 }void redfs(int now){if(tree[now].son[0]) redfs(tree[now].son[0]); 27     if(!tree[now].id)reb[++inr]=tree[now].val,ren[inr]=tree[now].num; 28     if(tree[now].son[1]) redfs(tree[now].son[1]);mem[++inm]=now;tree[now].res(); 29 }void rebuild(int l,int r,int pla,int f){if(l>r)return; 30     if(l==r){tree[pla].val=reb[l];tree[pla].siz=tree[pla].num=ren[l];tree[pla].fa=f;return;} 31     int mid=l+r>>1;tree[pla].val=reb[mid];int s1,s2;tree[pla].siz=tree[pla].num=ren[mid];tree[pla].fa=f; 32     if(l<mid){s1=tree[pla].son[0]=mem[inm--];rebuild(l,mid-1,s1,pla);} 33     if(mid<r){s2=tree[pla].son[1]=mem[inm--];rebuild(mid+1,r,s2,pla);}update(pla); 34 }void reset(int rot){ 35     int f=tree[rot].fa;inr=0;redfs(rot);rebuild(1,inr,((root==rot)?root=mem[inm--]:mem[inm--]),f); 36     for(;tree[rot].fa;rot=tree[rot].fa)update(tree[rot].fa); 37 }char need_to_reset(int now){return tree[now].siz*3<tree[tree[now].son[0]].siz*4
38     ||tree[now].siz*3<tree[tree[now].son[1]].siz*4||tree[now].siz<tree[now].hid*3; 39 }void insert(int now,int x,int &ntr){if(!now){int u=(inm?mem[inm--]:++ins); 40     tree[u].siz=tree[u].num=1;tree[u].val=x;root=u;return;} 41     if(tree[now].val==x){if(tree[now].id) tree[now].id=0,tree[now].hid--; 42         tree[now].siz++,tree[now].num++;if(need_to_reset(now)) ntr=now;return; 43     }if(tree[now].son[tree[now].val<x]) insert(tree[now].son[tree[now].val<x],x,ntr); 44     else{int u=(inm?mem[inm--]:++ins);tree[u].siz=tree[u].num=1;tree[u].fa=now; 45         tree[u].val=x;tree[now].son[tree[now].val<x]=u; 46     }update(now);if(need_to_reset(now)) ntr=now; 47 }void delet(int now,int x,int &ntr){ 48     if(tree[now].val==x){if(tree[now].num) tree[now].num--,tree[now].siz--,tree[now].hid++; 49         if(!tree[now].num) tree[now].id=1;if(need_to_reset(now)&&(tree[now].son[0]||tree[now].son[1])) ntr=now;return; 50     }if(tree[now].son[tree[now].val<x]) delet(tree[now].son[tree[now].val<x],x,ntr); 51     update(now);if(need_to_reset(now)) ntr=now; 52 }int findran(int x){int u=root,ans=1; 53     for(;u&&tree[u].val!=x;u=tree[u].son[x>tree[u].val]) ans+=(x>tree[u].val)*(tree[tree[u].son[x<tree[u].val]].siz+tree[u].num); 54     if(u) ans+=tree[tree[u].son[0]].siz;return ans; 55 }int findnum(int x){int u=root; 56     for(char t;tree[u].son[tree[tree[u].son[0]].siz+tree[u].num<x?1:0]&&(tree[tree[u].son[0]].siz>=x||tree[tree[u].son[0]].siz+tree[u].num<x); 57     t=tree[tree[u].son[0]].siz+tree[u].num<x,x-=t*(tree[tree[u].son[0]].siz+tree[u].num),u=tree[u].son[t]);return tree[u].val; 58 }int las(int x){int rnk=findran(x);return findnum(rnk-1);} 59 int nex(int x){int u=root,ans=0; 60     for(;u&&tree[u].val!=x;u=tree[u].son[x>tree[u].val]) ans+=(x>tree[u].val)*(tree[tree[u].son[x<tree[u].val]].siz+tree[u].num); 61     if(u) ans+=tree[tree[u].son[0]].siz+tree[u].num;return findnum(ans+1); 62 }int main(){ 63  read(n); 64     fui(i,1,n,1){ 65         int opt,x,y=0;read(opt);read(x); 66         switch(opt){ 67             case 1:insert(root,x,y);if(y) reset(y);break; 68             case 2:delet(root,x,y);if(y) reset(y);break; 69             case 3:cout<<findran(x)<<endl;break; 70             case 4:cout<<findnum(x)<<endl;break; 71             case 5:cout<<las(x)<<endl;break; 72             case 6:cout<<nex(x)<<endl; 73  } 74  } 75     return 0; 76 }
AC代碼
60分代碼

注意以上兩份代碼只有insert函數和delet函數不一樣。AC代碼爲遞歸式的,60分代碼爲循環式的。我估計是循環的時候沒法維護每一個節點的hid值(至於你說找到這個點後再循環回根update,本蒟蒻也打過,而且TLE,仍是60分。。。)

替罪羊樹的其餘做用

直接上題吧

洛谷P4278 帶插入區間K小值/BZOJ3065 帶插入區間K小值

解法之一:平衡樹套線段樹(替罪羊樹套主席樹)(然而本蒟蒻不會。。。)

(ps:這題基本別想在洛谷A掉。此題堪稱洛谷最難題。在BZOJ上4個點共60s,洛谷上5個點各1s,目前沒有一我的A掉)

附:關於平衡樹套線段樹

又是引進VFleaking大佬的(Orzzzzzz)

相關文章
相關標籤/搜索