替罪羊樹學習筆記
前言
替罪羊樹(Scapegoat Tree,SGT)是由 Arne Andersson 提出的一種非旋轉自平衡樹,可以做到單次均攤 \(O(\log n)\) 的時間複雜度內實現平衡樹的所有操作(時間複雜度基於勢能分析)。
替罪羊樹的優點:
- 不需要進行左旋、右旋、單旋、雙旋、伸展等基於旋轉操作。減少了碼量和思維量。
- 常數相對於常用的 Splay、FHQ Treap 較小(如 P6136,FHQ Treap 19.93 s,替罪羊樹 16.05 s 相差近 \(4\) 秒)。
替罪羊樹的缺點:
- 由於時間複雜度由勢能分析保證,因此無法可持久化(不過似乎有人寫了沒被卡)
- 無法實現分裂、合併操作,也無法實現文藝平衡樹。
前置姿勢:二叉搜尋樹。(可以認為替罪羊樹就是有平衡措施的二叉搜尋樹)
如果不會二叉搜尋樹建議看 「學習筆記」淺析BST二叉搜尋樹 - do_while_true - 部落格園 (cnblogs.com)。
約定
我們使用下面的結構體來儲存節點:
struct node{
int s,sz,sd,cnt,l,r,w;
void init(int weight){
w=weight;
l=r=0;
s=sz=sd=cnt=1;
}
} t[10000005];
其中:
\(\texttt{s}\) 表示以當前節點為根的子樹大小(不計重複元素,計刪除了卻保留下來的元素)(維護平衡使用)
\(\texttt{sz}\)
\(\texttt{sd}\) 表示以當前節點為根的子樹大小(不計重複元素,不計刪除了卻保留下來的元素)(維護平衡使用)
\(\texttt{cnt}\) 表示當前節點的元素個數。
\(\texttt{l,r}\) 分別表示當前節點的左、右子節點。
\(\texttt{w}\) 表示當前節點儲存的值。
\(\texttt{init(weight)}\) 函式表示新建一個值為 \(\texttt{weight}\) 的節點的邏輯。
除此之外,我們還定義以下巨集、變數:
#define ls (t[i].l) #define rs (t[i].r) int root,tot; const double alpha=0.7;
\(\texttt{ls,rs}\) 的意義不必多說,\(\texttt{root}\) 是當前的根,\(\texttt{tot}\) 是當前已經有過多少個節點。
\(\alpha(\texttt{alpha})\) 表示的是平衡因子,在後面會有介紹。
資訊上推
我們需要從子節點推出父節點的資訊,就需要使用資訊上推(Push Up)。
程式碼實現非常自然,就偷個懶,不講了,程式碼如下:
void pushup(int i){
t[i].s=t[ls].s+t[rs].s+1;
t[i].sz=t[ls].sz+t[rs].sz+t[i].cnt;
t[i].sd=t[ls].sd+t[rs].sd+(t[i].cnt>0);
}
維護平衡
總述
替罪羊樹是基於”重構“(Rebuild)來實現平衡的。
所謂重構,不過是將子樹打破直接暴力建出新樹而已。同時還會不保留刪除過的節點(\(\texttt{cnt}=0\))。
重構的契機
知道了什麼是重構,接下來我們來想一想什麼時候重構?
- 如果一個節點的子樹大小 \(s_1\) 佔到了這個節點的子樹大小 \(s\) 的大部分,那麼就需要重構這個節點。
- 如果一個節點的子樹中未被刪除的節點數 \(\texttt{sz}\) 佔到了這個節點的子樹大小 \(s\) 的小部分,那麼就需要重構這個節點。
具體的大部分、小部分是多少呢?我們用 \(\alpha\)(平衡因子)來定義,可以像這樣:
- 如果 \(\min\{s_{l},s_{r}\}\geq\alpha\cdot s\),那麼就需要重構這個節點。
- 如果 \(\texttt{sz}\leq\alpha\cdot s\),那麼就需要重構這個節點。
容易看出 \(\alpha\) 需要滿足 \(\frac{1}{2}\leq\alpha\lt1\)。在實際應用中,一般取 \(0.7\) 或 \(0.8\)。
然後我們就可以寫出一個判斷函式:
bool need_rebuild(int i){
if(t[i].cnt==0)return false;
if(alpha*t[i].s<=(double)(max(t[ls].s,t[rs].s)))return true;
if((double)t[i].sd<=alpha*t[i].s)return true;
return false;
}
下面可能會將【需要重構】稱之為【失衡】。
重構——平展
平展(Flatten)是重構中的前半部分,它指的是將一個子樹按中序遍歷展開。求中序遍歷。
Flatten 在英文中還有精簡的意思。在平展過程中我們也需要精簡節點。就是將刪完的節點(\(\texttt{cnt}=0\))從中序遍歷中刪除。
程式碼:
void flatten(int i,vector<int> &seq){
if(!i)return;
flatten(ls,seq);
if(t[i].cnt)seq.push_back(i);
flatten(rs,seq);
}
重構——建立
建立(Build)就是給出中序遍歷,建出二叉搜尋樹。當然我們需要讓建出來的數儘量平衡,只需要每一次選擇區間 \([l,r]\) 的中部即可。具體細節如果不會,建議重新從普及組學起。
程式碼如下:
int build(int l,int r,const vector<int> &seq){
if(l==r)return 0;
int mid=(l+r)>>1;
int i=seq[mid];
ls=build(l,mid,seq);
rs=build(mid+1,r,seq);
pushup(i);
return i;
}
重構實現
然後就成功實現重構部分了!貼個整體程式碼:
bool need_rebuild(int i){
if(t[i].cnt==0)return false;
if(alpha*t[i].s<=(double)(max(t[ls].s,t[rs].s)))return true;
if((double)t[i].sd<=alpha*t[i].s)return true;
return false;
}
namespace Rebuild{
void flatten(int i,vector<int> &seq){
if(!i)return;
flatten(ls,seq);
if(t[i].cnt)seq.push_back(i);
flatten(rs,seq);
}
int build(int l,int r,const vector<int> &seq){
if(l==r)return 0;
int mid=(l+r)>>1;
int i=seq[mid];
ls=build(l,mid,seq);
rs=build(mid+1,r,seq);
pushup(i);
return i;
}
}
void rebuild(int &i){
vector<int> seq;
seq.push_back(0);
Rebuild::flatten(i,seq);
i=Rebuild::build(1,seq.size(),seq);
}
其他基本操作
插入
插入(Insert)一個元素,與二叉搜尋樹類似,不過需要在遞迴完後上推資料,還需要判斷是否失衡,如果失衡,那麼就重構。
程式碼:
void newnode(int &i,int v){// 新建節點
i=(++tot);
if(!root)root=i;
t[i].init(v);
}
void insert(int &i,int v){
if(!i){
newnode(i,v);
return;
}
if(t[i].w==v)t[i].cnt++;
else if(t[i].w>v)insert(ls,v);
else insert(rs,v);
pushup(i);
if(need_rebuild(i))rebuild(i);
}
刪除
刪除(Remove)一個元素,我們使用其他大多數平衡樹使用的【懶刪除】策略。只是將沿途的 \(\texttt{sz}\) 減 \(1\)。如果到了要刪除的節點,那麼也要將 \(\texttt{cnt}\) 減 \(1\)。最後和插入一樣,也需要上推資料和判斷失衡。
程式碼:
void remove(int &i,int v){
if(!i)return;
t[i].sz--;
if(t[i].w==v){
if(t[i].cnt>0)t[i].cnt--;
return;
}
if(t[i].w>v)remove(ls,v);
else remove(rs,v);
pushup(i);
if(need_rebuild(i))rebuild(i);
}
查詢排名,查詢排名對應的元素,查前驅,查後繼
這一部分和二叉搜尋樹一模一樣。就不講了,只貼程式碼:
int kth(int &i,int k){
if(!i)return 0;
if(t[ls].sz>=k)return kth(ls,k);
if(t[ls].sz<k-t[i].cnt)return kth(rs,k-t[ls].sz-t[i].cnt);
return t[i].w;
}
int rnk(int &i,int v){
if(!i)return 1;
if(t[i].w>v)return rnk(ls,v);
if(t[i].w<v)return rnk(rs,v)+t[ls].sz+t[i].cnt;
return t[ls].sz+1;
}
int upper_bound(int &i,int v,bool great=0){
if(!i)return !great;
if(t[i].w==v&&t[i].cnt>0)return t[ls].sz+(!great)*(t[i].cnt+1);
if(!great){
if(v<t[i].w)return upper_bound(ls,v);
return upper_bound(rs,v)+t[ls].sz+t[i].cnt;
}
if(t[i].w<v)return upper_bound(rs,v,1)+t[ls].sz+t[i].cnt;
return upper_bound(ls,v,1);
}
int pre(int &i,int v){
return kth(i,upper_bound(i,v,1));
}
int next(int &i,int v){
return kth(i,upper_bound(i,v));
}
P6136 【模板】普通平衡樹(資料加強版)
模板題,程式碼如下:
顯示程式碼
#include <bits/stdc++.h>
#define int long long
using namespace std;
namespace ScapegoatTree{
#define ls (t[i].l)
#define rs (t[i].r)
struct node{
int s,sz,sd,cnt,l,r,w;
void init(int weight){
w=weight;
l=r=0;
s=sz=sd=cnt=1;
}
} t[10000005];
int root,tot;
const double alpha=0.7;
void pushup(int i){
t[i].s=t[ls].s+t[rs].s+1;
t[i].sz=t[ls].sz+t[rs].sz+t[i].cnt;
t[i].sd=t[ls].sd+t[rs].sd+(t[i].cnt>0);
}
bool need_rebuild(int i){
if(t[i].cnt==0)return false;
if(alpha*t[i].s<=(double)(max(t[ls].s,t[rs].s)))return true;
if((double)t[i].sd<=alpha*t[i].s)return true;
return false;
}
namespace Rebuild{
void flatten(int i,vector<int> &seq){
if(!i)return;
flatten(ls,seq);
if(t[i].cnt)seq.push_back(i);
flatten(rs,seq);
}
int build(int l,int r,const vector<int> &seq){
if(l==r)return 0;
int mid=(l+r)>>1;
int i=seq[mid];
ls=build(l,mid,seq);
rs=build(mid+1,r,seq);
pushup(i);
return i;
}
}
void rebuild(int &i){
vector<int> seq;
seq.push_back(0);
Rebuild::flatten(i,seq);
i=Rebuild::build(1,seq.size(),seq);
}
void newnode(int &i,int v){
i=(++tot);
if(!root)root=i;
t[i].init(v);
}
void insert(int &i,int v){
if(!i){
newnode(i,v);
return;
}
if(t[i].w==v)t[i].cnt++;
else if(t[i].w>v)insert(ls,v);
else insert(rs,v);
pushup(i);
if(need_rebuild(i))rebuild(i);
}
void remove(int &i,int v){
if(!i)return;
t[i].sz--;
if(t[i].w==v){
if(t[i].cnt>0)t[i].cnt--;
return;
}
if(t[i].w>v)remove(ls,v);
else remove(rs,v);
pushup(i);
if(need_rebuild(i))rebuild(i);
}
int kth(int &i,int k){
if(!i)return 0;
if(t[ls].sz>=k)return kth(ls,k);
if(t[ls].sz<k-t[i].cnt)return kth(rs,k-t[ls].sz-t[i].cnt);
return t[i].w;
}
int rnk(int &i,int v){
if(!i)return 1;
if(t[i].w>v)return rnk(ls,v);
if(t[i].w<v)return rnk(rs,v)+t[ls].sz+t[i].cnt;
return t[ls].sz+1;
}
int upper_bound(int &i,int v,bool great=0){
if(!i)return !great;
if(t[i].w==v&&t[i].cnt>0)return t[ls].sz+(!great)*(t[i].cnt+1);
if(!great){
if(v<t[i].w)return upper_bound(ls,v);
return upper_bound(rs,v)+t[ls].sz+t[i].cnt;
}
if(t[i].w<v)return upper_bound(rs,v,1)+t[ls].sz+t[i].cnt;
return upper_bound(ls,v,1);
}
int pre(int &i,int v){
return kth(i,upper_bound(i,v,1));
}
int next(int &i,int v){
return kth(i,upper_bound(i,v));
}
}
int last=0,ans=0;
signed main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int m,n;cin>>m>>n;
while(m--){
int v;cin>>v;ScapegoatTree::insert(ScapegoatTree::root,v);
}
while(n--){
int op,x;
cin>>op>>x;
x^=last;
if(op==1)ScapegoatTree::insert(ScapegoatTree::root,x);
if(op==2)ScapegoatTree::remove(ScapegoatTree::root,x);
if(op==3)last=ScapegoatTree::rnk(ScapegoatTree::root,x);
if(op==4)last=ScapegoatTree::kth(ScapegoatTree::root,x);
if(op==5)last=ScapegoatTree::pre(ScapegoatTree::root,x);
if(op==6)last=ScapegoatTree::next(ScapegoatTree::root,x);
if(op==3||op==4||op==5||op==6){
ans^=last;
}
}
cout<<ans;
return 0;
}