替罪羊樹是一種依靠重構操作維持平衡的重量平衡樹。替罪羊樹會在插入、刪除操作時,檢測凈土的節點,若發現失衡,則將以該節點為根的子樹重構。
序言:
我們知道在一棵平衡的二叉搜索樹內進行查詢等操作時,時間就可以穩定在\(log(n)\)但是每一次的插入和刪除節點,都會使得這棵樹不平衡,最會情況就是退化成一條鏈,顯然我們不想要這種樹,於是各種維護的方法出現了,有通過旋轉的,有拆樹在合並的,然而替罪羊樹就很優美的,因為一旦發現不平衡的子樹,立即拍扁重構,於是替罪羊樹的核心是:暴力重建
正題:
替罪羊樹的每個節點都包含什么:
\(left\),\(right\):記錄該節點的左右兒子
\(x\):該節點的值
\(tot\):有多少個值為\(x\)的數
\(sz,trsz,whsz\):\(sz\)表示以該節點為根的子樹內有多少個節點,\(trsz\)表示有多少個有效節點,\(whsz\)表示有多少個數(也就數子樹內所有節點的\(tot\)的和)
\(fa\):該點的父親
\(vis\):該點是否有刪除標記
操作一:加點
先找到一個特殊的節點,如果那個節點的值等於要加的那個點,那么直接讓那個節點的\(tot+1\)即可,如果比那個節點的值要小,就讓新加的節點稱為它的左兒子,大的話就是右兒子。
那么如何找到那個"特殊的節點"?假如我們以\(x\)為關鍵字去查找,先從根節點開始,假如\(x\)比根節點的值要小,那我就去它的左兒子哪里,不然去右兒子,直到滿足以下兩個條件之一:
- 找到了值為\(x\)的節點
- 不能繼續往下找
那么這個所謂的特殊的節點的性質也就很顯然,就是與新加入的節點值相同的點,或者新加入的節點的前驅或后繼
找點:
int find(int x,int now){//now表示當前找到哪個點
if(x < tr[now].x && tr[now].left) return find(x,tr[now].left);//比當前點的值要小並且有左兒子
if(x > tr[now].x && tr[now].right) return find(x,tr[now].right);
return now;
}
加點:
void add(int x){
if(root == 0){//假如當前沒有根節點,也就是當前的樹是空的,那么直接讓他成為根
build(x,root = New(),0);//新建節點(后面有講)
return;
}
int p = find(x,root);//找到特殊點
if(x == tr[p].x{
tr[p].tot++;
if(tr[p].vis) tr[p].vis = 0,updata(p,1,0,1);
else updata(p,0,0,1);
}
else if(x < tr[p].x) build(x,tr[p].left = New(),p),updata(p,1,1,1);
else build(x,tr[p].right = New(),p),updata(p,1,1,1);
find_rebuild(root,x);
}
上面用到的幾個函數:
新建節點:
void build(int x,int y,int fa){//初始化樹上編號為y的節點,它的值為x,父親為fa
tr[y].left = tree[y].right = 0;tr[y].fa = fa;tree[y].vis = 0;
tr[y].x = x;tr[y].tot = tr[y].sz = tr[y].trsz = tr[y].whsz = 1;
}
\(updata\)函數,更新父親以及爺爺以及祖先們的\(sz,trsz還有whsz\):
void updata(int x,int y,int z,int k){
if(!x) return;
tr[x].trsz += y;
tr[x].sz += z;
tr[x].whsz += k;
updata(tr[x].fa,y,z,k);
}
\(New\)函數就是個內存池。
操作二:刪點
刪點,嚴格來說是刪掉一個數,假如我們要刪掉一個值為\(x\)的數,那就先找到值為\(x\)的節點,然后\(tot-1\)
難道就這么簡單么?當然不是,假如\(tot-1\)之后變成\(0\)了怎么辦?這意味着這個節點不存在了,然后我們刪掉這個節點么?如果把它刪了,他的左右兒子怎么辦?所以我們不能動他,給它打個標記,標記它被刪除了
代碼:
void del(int x){
int p = find(x,root);
tr[p].tot--;
if(!tr[p].tot) tr[p].vis = true,updata(p,-1,0,-1);
else updata(p,0,0,-1);
find_rebuild(root,x);
}
我們上面的代碼提到了一個函數\(find\)_\(rebuild\),每一次的加點和刪點都有可能使這棵樹不平衡,假如有一顆子樹不平衡,我們就需要將其重建,所以,\(find\) _\(rebuild\)就是用來查找需要重建的子樹。
先說一下怎么重建。
因為需要重建的子樹比訂書二叉搜索樹,那么這棵子樹的中序遍歷一定是一個嚴格上升的序列,於是我們就先中序遍歷一下,把樹上的有效節點放到一個數組里里面,注意無效節點(就是被打了刪除標記的節點)不要。
然后我們在把數組中的節點重建成一棵極其平衡的完全二叉樹(按完全二叉樹的方法來建),具體放大就是每一次選取數組中間的節點,讓它成為跟,左邊的當它左子樹,右邊的當它右子樹,然后再對左右兒子進行相同的操作。
怎么找需要重建的子樹:
我們每次\(add\)或\(del\)的數為\(x\),在將這個數加入到樹中或從樹中刪除之后,加入在樹中值為\(x\)的節點是\(y\),那我們考慮到其實每一次可能小重構的子樹只會是以“根到\(y\)路徑上的節點”為根的子樹,那么我們就可以從根往\(y\)走一次,看看誰要重建就好了。不是\(y\)往根走的原因:如果根到\(y\)的路徑上只有兩個點\(a,b\),並且\(a\)是\(b\)的祖先,然后特別巧的是\(a,b\)都是需要重建的,那么這個時候我們只要重建祖先節點為根的子樹,因為重建之后,另一個為根的子樹在其內部也重建完了,如果\(y\)往根走就會出現重建兩遍的情況。
判斷替罪羊樹是否平衡:
在替罪羊樹中定義了個一個平衡因子\(\alpha\),\(\alpha\)的范圍因題而異,一般在\(0.5-1.0\)之間。判斷一棵子樹是否平衡的方法:如果\(x\)的左(右)兒子的節點數量大於以\(x\)為根的子樹的節點數量\(*\alpha\),那么以\(x\)為根的這棵子樹就是不平衡的,這時就將它重建。
還有一種情況就是,打了刪除標記的點多了,效率自然會變慢,所欲如果在一棵子樹內有超過30%的幾點被打了刪除標記,就把這棵樹重建。
\(find\)_\(rebuild\):
void find_rebuild(int now,int x){
if(1.0 * tr[tr[now].left].sz > 1.0 * tr[now].sz * alpha || 1.0 * tr[tr[now].right].sz > 1.0 * tr[now].sz * alpha|| 1.0 * tr[now].sz - 1.0 * tr[now].trsz >1.0 * tr[now].sz * 0.3){
rebuild(now);
return;
}
if(tr[now].x != x) find_rebuild(x < tr[now].x ? tr[now].left : tree[now].right,x);
}
\(rebuild\):
void rebuild(int x){//重建以x為根的子樹
tt = 0;
dfs_rebuild(x);//進行中序遍歷並將有效節點壓入數組
if(x == root) root = readd(1,tt,0);//x就是根,那么root就變成重建之后的那棵樹的根
//readd用來把數組里的節點重新建成一棵完全二叉樹,並返回這棵樹的根
else{
updata(tr[x].fa,0,-(tr[x].sz - tree[x].trsz),0);//因為拍扁重建后的樹中,被打了刪除標記的節點將消失,所以要將祖先們的size進行更改,也就是減去被刪去的節點
if(tr[tr[x].fa].left == x) tr[tr[x].fa].left = readd(1,tt,tr[x].fa);
else tr[tr[x].fa].right = readd(1,tt,tr[x].fa);
}
}
\(readd\):
int readd(int l,int r,int fa){
if(l > r)return 0
int mid = (l + r) >> 1;//選中間的點作為根
int id = New();
tree[id].fa = fa;//更新各項
tree[id].tot = num[mid].tot;
tree[id].x = num[mid].x;
tree[id].left = readd(l,mid - 1,id);
tree[id].right = readd(mid + 1,r,id);
tree[id].whsz = tr[tr[id].left].whsz + tr[tr[id].right].whsz + num[mid].tot;
tree[id].sz = tr[id].trsz = r - l + 1;
tree[id].vis = 0;
return id;
}
中序遍歷:
void dfs_rebuild(int x){
if(x == 0) return;
dfs_rebuild(tr[x].left);
if(!tr[x].vis) num[++tt].x = tr[x].x,num[tt].tot = tree[x].tot;//假如沒有刪除標記,就只將他的x和tot加進數組,因為其他東西都沒有用
ck[++t] = x;//倉庫,存下廢棄的節點
dfs_rebuild(tr[x].right);
}
然后就是\(New\):
int New(){
if(t > 0) return ck[t--];//假如倉庫內有點,就直接用
else return ++len;//否則再創造一個點
}
然后我們就可以進行剩下的幾個基本操作了
操作三:查找\(x\)的排名:
我們只需要像\(find\)函數一樣走一遍行。如果往右兒子走,就讓\(ans\)加上左兒子的數的個數,再加上當前節點的\(tot\),否則就往左兒子走,走到值為\(x\)的點結束。
void findx(int x){
int now = root;
int ans = 0;
while(tr[now].x != x){
if(x < tr[now].x) now = tr[now].left;
else ans += tr[tr[now].left].whsz + tr[now].tot,now = tr[now].right;
}
ans += tr[tr[now].left].whsz;
printf("%d\n",ans + 1);
}
操作四:查找排名為\(x\)的數
類似的,我們先從根走,假如當前節點的左子樹的數的個數比\(x\)要小,那么讓\(x\)減掉左子樹的數的個數,然后在看一下當前節點的\(tot\)是否大於\(x\),如果大於的話,答案就是這個節點了,否則讓\(x\)減去它的\(tot\),然后往右兒子去,重復以上操作即可。
void findrkx(int x){
int now = root;
while(1){
if(x <= tr[tr[now].left].whsz) now = tr[now].left;
else{
x -= tr[tr[now].left].whsz;
if(x <= tr[now].tot){
printf("%d\n",tr[now].x);
return;
}
x -= tr[now].tot;
now = tr[now].right;
}
}
}
要注意!這兩個函數里用的都是\(whsz\)
操作五:查找\(x\)的前驅
因為替罪羊樹有刪除標記這個東西,所以它查找前驅和后繼的時候會慢一點。
具體做法:先找到值為\(x\)的節點,然后普看看有沒有左兒子,如果有就將左子樹遍歷一遍,順序是:右兒子->根->左兒子,找到第一個沒有被刪除的節點就是答案。
因為被刪除的點不超過30%,所以不用擔心算法會退化成\(O(n)\)
void dfs_rml(int x){
if(tr[x].right != 0) dfs_rml(tr[x].right);
if(ans) return;
if(!tr[x].vis){
printf("%d\n",tr[x].x);
ans = 1;
return;
}
if(tr[x].left != 0) dfs_rml(tr[x].left);
}
void pre(int now,int x,bool z){
if(!z){
pre(tr[now].fa,x,tr[tr[now].fa].right == now);
return;
}
if(!tr[now].vis && tr[now].x < x){
printf("%d\n",tr[now].x);
return;
}
if(tr[now].left){
ans = 0;
dfs_rml(tr[now].left);
return;
}
pre(tr[now].fa,x,tr[tr[now].fa].right == now);
}
操作六:查找\(x\)的后繼
跟前驅類似
void dfs_lmr(int x){
if(tr[x].left != 0) dfs_lmr(tr[x].left);
if(ans) return;
if(!tr[x].vis){
printf("%d\n",tre[x].x);
ans = 1;
return;
}
if(tr[x].right != 0) dfs_lmr(tr[x].right);
}
void nxt(int now,int x,bool z){
if(!z){
nxt(tr[now].fa,x,tr[tr[now].fa].right != now);
return;
}
if(!tr[now].vis && tr[now].x > x){
printf("%d\n",tr[now].x);
return;
}
if(tr[now].right){
ans = 0;
dfs_lmr(tr[now].right);
return;
}
nxt(tr[now].fa,x,tr[tr[now].fa].right != now);
}
后記:
-
關於\(\alpha\):
\(\alpha\)的值究竟與效率的關系,當的\(\alpha\)值越小,那么替罪羊樹就越容易重構,那么樹也就越平衡,查詢的效率也就越高,自然修改(加點和刪點)的效率也就低了。所以,如果查詢操作比較多的話,就可以將\(\alpha\)的值設小一點。反之,假如修改操作多,自然\(\alpha\)的值就要大一點了。
還有,\(\alpha\)不能等於\(1\) \(or\) \(0.5\),假如它等於\(0.5\),那么當一棵樹被重構之后如果因為節點數問題,不能完全重構成一個完全二叉樹,那么顯然,對於這棵樹的根,他的"左子樹節點數量 - 右子樹節點數量"很可能會等於\(1\),那么如果往多的那棵子樹上加一個節點,那么這棵樹又得重構一次,最壞情況時間會變成\(n^2\)。那么等於1...會有一棵子樹的大小大於整棵樹的大小咩w?
-
關於時間復雜度:
除了重構操作,其他操作的時間復雜度顯然都是\(log(n)\)的,那么下面看一下重構的時間復雜度。
雖然重構一次的時間復雜度是\(O(n)\)的,但是,均攤下來其實只是\(O(logn)\)。
考慮極端情況,每次都把整棵樹重構。
那么我們就需要每次都往根的一棵子樹內加點,假設一開始是平衡的,那么左右子樹各有50%的節點,那么要使一棵子樹內含有超過75%的節點,那么這棵子樹就需要在原來的基礎上增加\(2\)倍的節點數。也就是說,當最差情況時,整棵替罪羊樹的節點數要翻個倍,才會重構。那么最差情況時也就是在\(4,8,16,32……\)個節點時才會重構,於是重構的總的時間復雜度也就是\(O(nlogn)\)了,加上一些雜七雜八的重構,也不過就是加上一個很小的常數,可以省略不計。所以,替罪羊樹的時間復雜度依然是\(O(nlogn)\)的。
完整代碼
#define B cout << "BreakPoint" << endl;
#define O(x) cout << #x << " " << x << endl;
#define O_(x) cout << #x << " " << x << " ";
#define Msz(x) cout << "Sizeof " << #x << " " << sizeof(x)/1024/1024 << " MB" << endl;
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#define LL long long
const int inf = 1e9 + 9;
const int N = 1e7 + 5;
using namespace std;
inline int read() {
int s = 0,w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
w = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
s = s * 10 + ch - '0';
ch = getchar();
}
return s * w;
}
struct node{
int left,right,x,tot,sz,trsz,whsz,fa;
bool vis;
} tr[N];
struct sl{
int x,tot;
}num[N];
int len,n,root,ck[N],t;
double alpha = 0.75;
void build(int x,int y,int fa){
tr[y].left = tr[y].right = 0;
tr[y].fa = fa,tr[y].vis = false;
tr[y].x = x,tr[y].tot = tr[y].sz = tr[y].trsz = tr[y].whsz = 1;
}
inline int New(){
if(t > 0) return ck[t--];
else return ++len;
}
void updata(int x,int y,int z,int k){
if(!x) return;
tr[x].trsz += y;
tr[x].sz += z;
tr[x].whsz += k;
updata(tr[x].fa,y,z,k);
}
int find(int x,int now){
if(x < tr[now].x && tr[now].left) return find(x,tr[now].left);
if(x > tr[now].x && tr[now].right) return find(x,tr[now].right);
return now;
}
int tt;
void dfs_rebuild(int x){
if(x == 0)return;
dfs_rebuild(tr[x].left);
if(!tr[x].vis) num[++tt].x = tr[x].x,num[tt].tot = tr[x].tot;
ck[++t] = x;
dfs_rebuild(tr[x].right);
}
int readd(int l,int r,int fa){
if(l > r) return 0;
int mid = (l+r)>>1;
int id = New();
tr[id].fa = fa;
tr[id].tot = num[mid].tot;
tr[id].x = num[mid].x;
tr[id].left = readd(l,mid-1,id);
tr[id].right = readd(mid+1,r,id);
tr[id].whsz = tr[tr[id].left].whsz + tr[tr[id].right].whsz + num[mid].tot;
tr[id].sz = tr[id].trsz = r - l + 1;
tr[id].vis = false;
return id;
}
void rebuild(int x){
tt = 0;
dfs_rebuild(x);
if(x == root) root = readd(1,tt,0);
else{
updata(tr[x].fa,0,-tr[x].sz + tr[x].trsz,0);
if(tr[tr[x].fa].left == x) tr[tr[x].fa].left = readd(1,tt,tr[x].fa);
else tr[tr[x].fa].right = readd(1,tt,tr[x].fa);
}
}
void find_rebuild(int now,int x){
if(1.0 * tr[tr[now].left].sz > 1.0 * tr[now].sz * alpha || 1.0 * tr[tr[now].right].sz > 1.0 * tr[now].sz * alpha|| 1.0 * tr[now].sz - 1.0 * tr[now].trsz >1.0 * tr[now].sz * 0.3){
rebuild(now);
return;
}
if(tr[now].x != x) find_rebuild(x < tr[now].x ? tr[now].left : tr[now].right,x);
}
void add(int x){
if(root == 0){
build(x,root = New(),0);
return;
}
int p = find(x,root);
if(x == tr[p].x){
tr[p].tot++;
if(tr[p].vis) tr[p].vis = 0,updata(p,1,0,1);
else updata(p,0,0,1);
}
else if(x < tr[p].x) build(x,tr[p].left = New(),p),updata(p,1,1,1);
else build(x,tr[p].right = New(),p),updata(p,1,1,1);
find_rebuild(root,x);
}
void del(int x){
int p = find(x,root);
tr[p].tot--;
if(!tr[p].tot) tr[p].vis = 1,updata(p,-1,0,-1);
else updata(p,0,0,-1);
find_rebuild(root,x);
}
void findx(int x){
int now = root;
int ans = 0;
while(tr[now].x != x){
if(x < tr[now].x) now = tr[now].left;
else ans += tr[tr[now].left].whsz + tr[now].tot,now = tr[now].right;
}
ans += tr[tr[now].left].whsz;
printf("%d\n",ans + 1);
}
void findrkx(int x){
int now = root;
while(1){
if(x <= tr[tr[now].left].whsz) now = tr[now].left;
else{
x -= tr[tr[now].left].whsz;
if(x <= tr[now].tot){
printf("%d\n",tr[now].x);
return;
}
x -= tr[now].tot;
now = tr[now].right;
}
}
}
bool ans;
void dfs_rml(int x){
if(tr[x].right != 0) dfs_rml(tr[x].right);
if(ans) return;
if(!tr[x].vis){
printf("%d\n",tr[x].x);
ans = 1;
return;
}
if(tr[x].left != 0) dfs_rml(tr[x].left);
}
void pre(int now,int x,bool z){
if(!z){
pre(tr[now].fa,x,tr[tr[now].fa].right == now);
return;
}
if(!tr[now].vis && tr[now].x < x){
printf("%d\n",tr[now].x);
return;
}
if(tr[now].left){
ans = 0;
dfs_rml(tr[now].left);
return;
}
pre(tr[now].fa,x,tr[tr[now].fa].right == now);
}
void dfs_lmr(int x){
if(tr[x].left != 0) dfs_lmr(tr[x].left);
if(ans) return;
if(!tr[x].vis){
printf("%d\n",tr[x].x);
ans = 1;
return;
}
if(tr[x].right != 0) dfs_lmr(tr[x].right);
}
void nxt(int now,int x,bool z){
if(!z){
nxt(tr[now].fa,x,tr[tr[now].fa].right != now);
return;
}
if(!tr[now].vis && tr[now].x > x){
printf("%d\n",tr[now].x);
return;
}
if(tr[now].right){
ans = 0;
dfs_lmr(tr[now].right);
return;
}
nxt(tr[now].fa,x,tr[tr[now].fa].right != now);
}
int main(){
n = read();
while(n--){
int id = read(),x = read();
if(id == 1) add(x);
if(id == 2) del(x);
if(id == 3) findx(x);
if(id == 4) findrkx(x);
if(id == 5) pre(find(x,root),x,1);
if(id == 6) nxt(find(x,root),x,1);
}
}
