1. 程式人生 > 其它 >替罪羊樹學習筆記

替罪羊樹學習筆記

前言

替罪羊樹(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;
}