教練講旋轉的時候摸魚去了,然後就不會旋轉操作了t_t,那怎麼辦呢,要做題的啊,誒,替罪羊樹好像是不用旋轉的誒qwq,就它了。
替罪羊樹這樣直接講不直觀,還是看題來講吧。
上題:洛谷 p3369 【模板】普通平衡樹
概念/思想
替罪羊樹屬於平衡樹的一種,但是他維護平衡的方式不是複雜的旋轉,而是直接把這棵子樹直接掰下來拍扁重建,再放回去就okk了所以暴力天下第一啊。
由於拍扁重建用的是二分,所以建樹的複雜度最多為為\(o(logn)\),具查詢的時候也是平衡的,複雜度也就為\(o(logn)\)了,還是很快的。
實現:變數名的定義/含義:
struct node;
node a[400040];
int root/*根節點*/ , pool/*記憶體池的指標*/ , poi/*儲存中序遍歷後的陣列的指標*/ , n;
double alpha = 0.75;
int memo[400040]/*記憶體池*/ , c[400040]/*儲存中序遍歷後的數*/;
記憶體池:因為動態開點會很慢,不如提前申請空間,要用的時候從裡面取就是了。注意!因為我們程式裡面的樹層序遍歷的點並不是有序的,所以這樣給點是沒問題的(就像有可能節點\(3\)的兒子可能是\(6\)和\(11\),而父親又為\(33\),因為我們這裡是編號,而不是點的值)。
而為什麼說\(tsize\)和\(fsize\)分為包括了刪除了的和不包括刪除了的呢?因為替罪羊樹是惰性刪除,只是打個標記而已,真正的刪除在重建的時候。
核心操作:判斷是否拍扁
這裡對於一棵樹是否需要拍扁,我們用到乙個東西,叫平衡因子,當這個樹總的大小乘乙個平衡因子的時候,還沒有其中大的那個兒子的子樹大時,就重建。平衡因子一般取\(0.5\)
$1$,但是由於太小的話,拍扁次數就太多了,太大的話,拍扁次數太少了,都不行,所以一般是去$0.7$
\(0.8\),我一般用\(0.75\)。
bool cheak(int now)
插入:
這個就跟普通的二叉查詢樹一樣的了,只不過插完後要判斷一下是否重建。
void insert(int &now , int t)
a[now].fsize++ , a[now].tsize++; //下面會更新乙個節點,所以上面的每乙個節點都要加一
if(a[now].date >= t) insert(a[now].ls , t);
else insert(a[now].rs , t);
if(!cheak(now)) rebuild(now); //插入乙個點後可能會不平衡的
}
建樹/重建:
當我們的需要重建時,這顆樹也一定是一顆二叉查詢樹,所以中序遍歷一定是有序的,所以我們只需要進行一次中序遍歷,然後對這個序列,從中間開始往兩邊二分進行建樹(如果這裡不是很懂的話可以先看看**,然後手推一下,為什麼對於乙個有序序列從中間開始建樹是平衡的)。
void dfs(int now)
void build(int l , int r , int &now)
if(l < mid) build(l , mid - 1 , a[now].ls); //在l=mid的時候再進行就會出現l>r的情況,直接令他左兒子為0就可以了
else a[now].ls = 0;
build(mid + 1 , r , a[now].rs); //因為除法總是向下取整,所以mid總是小於r的,所以不會出現l>r的情況
a[now].fsize = a[a[now].ls].fsize + a[a[now].rs].fsize + 1; //更新
a[now].tsize = a[a[now].ls].tsize + a[a[now].rs].tsize + 1;
}void rebuild(int &now)
查詢排名為rk的數:
這個也跟二叉查詢樹一樣的,不斷判斷,然後看是否到達即可。注意判斷的那裡因為惰性標記所以不一樣(具體為什麼可以看下面的delet刪除函式)。
int ft(int rk)
}}
查詢數t的排名:
也跟二叉查詢數一樣,注意下對排名的統計是每次走右子樹時累加左子樹的個數(具體為什麼還是看delet刪除函式)。
int frk(int t)
} return ans;
}
刪除排名為rk的數/刪除數k:
這裡的刪除只是打上標記而已,具體看**吧~(上面的問題也都在**裡面)
void delet(int &now , int rk)
a[now].fsize--; //跟insert那個操作很像,下面刪掉乙個節點,所以乙個將fsize減一,但是tsize不用減
if(a[a[now].ls].fsize + a[now].f >= rk) delet(a[now].ls , rk);
else delet(a[now].rs , rk - a[a[now].ls].fsize - a[now].f);
/*解釋一下為什麼rk要減去左子樹和根節點
因為我們來到的是一顆新的子樹,要減去左邊的排名才可以進入新的子樹,相當於把樹不斷變小,排名也不斷變小了
但是左子樹為什麼不用減呢?
因為我們某個點的排名應該是從這個點往右子樹走時累加左子樹之和,所以這裡是不能累加排名的(可以看下ft和frk函式裡面的操作)
這個地方可能很不好理解,可以自己畫圖模擬一下,會好很多
這可能就是只可意會不可言傳吧~*/
}void rdelet(int t)
上完整**:
#include using namespace std;
struct node;
node a[400040];
int root/*根節點*/ , pool/*記憶體池的指標*/ , poi/*儲存中序遍歷後的陣列的指標*/ , n;
double alpha = 0.75;
int memo[400040]/*記憶體池*/ , c[400040]/*儲存中序遍歷後的數*/;
bool cheak(int now)
void dfs(int now)
void build(int l , int r , int &now)
if(l < mid) build(l , mid - 1 , a[now].ls); //在l=mid的時候再進行就會出現l>r的情況,直接令他左兒子為0就可以了
else a[now].ls = 0;
build(mid + 1 , r , a[now].rs); //因為除法總是向下取整,所以mid總是小於r的,所以不會出現l>r的情況
a[now].fsize = a[a[now].ls].fsize + a[a[now].rs].fsize + 1; //更新
a[now].tsize = a[a[now].ls].tsize + a[a[now].rs].tsize + 1;
}void rebuild(int &now)
void insert(int &now , int t)
a[now].fsize++ , a[now].tsize++; //下面會更新乙個節點,所以上面的每乙個節點都要加一
if(a[now].date >= t) insert(a[now].ls , t);
else insert(a[now].rs , t);
if(!cheak(now)) rebuild(now); //插入乙個點後可能會不平衡的
}void delet(int &now , int rk)
a[now].fsize--; //跟insert那個操作很像,下面刪掉乙個節點,所以乙個將fsize減一,但是tsize不用減
if(a[a[now].ls].fsize + a[now].f >= rk) delet(a[now].ls , rk);
else delet(a[now].rs , rk - a[a[now].ls].fsize - a[now].f);
/*解釋一下為什麼rk要減去左子樹和根節點
因為我們來到的是一顆新的子樹,要減去左邊的排名才可以進入新的子樹,相當於把樹不斷變小,排名也不斷變小了
但是左子樹為什麼不用減呢?
因為我們某個點的排名應該是從這個點往右子樹走時累加左子樹之和,所以這裡是不能累加排名的(可以看下ft和frk函式裡面的操作)
這個地方可能很不好理解,可以自己畫圖模擬一下,會好很多
這可能就是只可意會不可言傳吧~*/
}int ft(int rk) }}
int frk(int t)
} return ans;
}void rdelet(int t)
int main()
return 0;
}
個人認為替罪羊樹比其他的平衡樹更好寫和好用(只是不支援區間操作qwq),時間複雜度也不高,算是比較良心的了。 替罪羊樹學習筆記
部落格咕咕咕了好久 最近會逐步繼續恢復更新部落格的。最近又在學習二叉搜尋樹。實測發現替罪羊樹快的飛起 時間約splay的1 2 寫起來還比較簡單,決定來一波。那為什麼還要用splay呢?因為splay是序列之王!還能維護lct!你要用非旋treap fhq treap 我也沒意見 替罪羊樹的主要思想...
替罪羊樹學習筆記
替罪羊樹是一種平衡樹。然而它既不能可持久化,又不能維護區間。所以把它發明出來幹嘛?然而它可以維護子樹內資訊。這個是一眾平衡樹都不能做到的功能。它維護平衡的方式和普通的平衡樹瘋狂旋轉不太一樣。它是如果這個子樹不平衡就把這個子樹拍平了重構。具體地,如果左子樹的節點數和右子樹的節點數比值超過某個值或小於某...
總結 替罪羊樹學習筆記
突然不會寫學習筆記了.反正是給自己看的 那就想到哪寫到哪吧 不基於旋轉而基於暴力重構的平衡樹 採用懶惰刪除法 每次刪除只打標記,在重構時統一刪除 如果某個點的子樹過於不平衡或刪除了過多的元素 那就需要進行重構 重構時暴力dfs整棵子樹,碰見打了刪除標記的節點就扔進記憶體池,否則按照中序遍歷存下來,然...