浅谈平衡树
什么是平衡樹
平衡樹其實是二叉搜索樹的優化,滿足 BST 1 性質。
關于平衡樹的種類其實有很多,但本文不涉及太多,我們講講最常用的 3 3 3 中平衡樹吧。
- T r e a p Treap Treap
- f h q T r e a p fhq \ Treap fhq?Treap
- S p l a y Splay Splay
先想想二叉搜索樹為什么要優化 ?
當然是因為如果我們要插入一連串且非常多的數時,二叉搜索樹會被卡成一條鏈。
而平衡樹又是怎么優化的呢 ?
其實是在滿足 BST 1 性質時,通過旋轉將樹拍扁,這樣就可以優化時間復雜度。
平衡樹模板
要求支持一下操作:
小細節:有時候需要在平衡樹中插入正無窮大和負無窮大,以便確定邊界。
Treap
T r e a p Treap Treap,顧名思義就是 T r e e ( 樹 ) + H e a p ( 堆 ) Tree(樹)+Heap(堆) Tree(樹)+Heap(堆) ,說明 T r e a p Treap Treap 不僅滿足 BST 1 性質,還滿足堆的性質 2。
算法思想
旋轉
T r e a p Treap Treap 的主要思想其實是左旋和右旋。
右旋的操作流程是:假如要對點 y y y 進行右旋,且 x x x 是 y y y 的左兒子,那么 y y y 就會成為點 x x x 的右兒子。此時若 x x x 原本有右兒子,則會發生沖突,所以還要令 x x x 原本的右兒子變成 y y y 的左兒子,此時仍然滿足 BST 1 性質。左旋的操作流程類似,只是把方向調換過來。具體見下圖:
因為我們不知道何時旋轉,所以 聽 天 由 命 \color{SpringGreen}{聽天由命} 聽天由命 吧。
即除了二叉搜索樹上的權值之外,我們還可以給每個節點加上一個隨機的權值。
接下來,我們利用左旋和右旋,將樹的形態調整至滿足如下性質:
-
對于原本的權值,這棵樹必須滿足二叉搜索樹的 BST 1 性質,即節點 x x x 原本的權值一定大于其左子樹中任意一個節點的原本權值且小于其右子樹中任意一個節點的原本權值。
-
對于隨機權值,這棵樹必須滿足堆的性質 2,即節點 x x x 的隨機權值比其兩個子節點的隨機權值都大或者都小,通常我們選用大根堆。
所以對于隨機數據,Treap 有很優秀的時間復雜度。
void Rotate(int &id, int d) {int temp = ch[id][d ^ 1];ch[id][d ^ 1] = ch[temp][d];ch[temp][d] = id;id = temp;pushup(ch[id][d]), pushup(id); }更新節點大小
c n t cnt cnt 數組記錄的是有多少個點與當前點權值相同(包括當前點),因為我這里是將權值一樣的節點合到了一個節點上。
void pushup(int id) {siz[id] = siz[ch[id][0]] + siz[ch[id][1]] + cnt[id]; }新建節點
int New(int v) {val[++tot] = v;dat[tot] = rand();siz[tot] = 1;cnt[tot] = 1;return tot; }插入
利用 BST 1 性質插入節點,小于插到左邊,大于插到右邊,沒有就新建,然后如果子節點的優先級要大于父節點,我們就把子節點旋轉上去。
void insert(int &id, int v) {if (!id){id = New(v);return;}if (v == val[id])cnt[id]++;else{int d = v < val[id] ? 0 : 1;insert(ch[id][d], v);if (dat[id] < dat[ch[id][d]])Rotate(id, d ^ 1);}pushup(id); }刪除
將這個點優先級大的子節點旋轉上來,自己就會旋轉下去,一直將其旋到葉子,然后刪除。
void Remove(int &id, int v) {if (!id)return;if (v == val[id]){if (cnt[id] > 1){cnt[id]--, pushup(id);return;}if (ch[id][0] || ch[id][1]){if (!ch[id][1] || dat[ch[id][0]] > dat[ch[id][1]]){Rotate(id, 1), Remove(ch[id][1], v);}elseRotate(id, 0), Remove(ch[id][0], v);pushup(id);}elseid = 0;return;}v < val[id] ? Remove(ch[id][0], v) : Remove(ch[id][1], v);pushup(id); }查找排名
其實也很好理解,如果當前點權值小于要找的數,就去左子樹找,否則去右子樹找。
int get_rank(int id, int v) {if (!id)return 0;if (v == val[id])return siz[ch[id][0]] + 1;else if (v < val[id])return get_rank(ch[id][0], v);elsereturn siz[ch[id][0]] + cnt[id] + get_rank(ch[id][1], v); }查找數值
這個就更簡單了不是嗎?
int get_val(int id, int rank) {if (!id)return INF;if (rank <= siz[ch[id][0]])return get_val(ch[id][0], rank);else if (rank <= siz[ch[id][0]] + cnt[id])return val[id];elsereturn get_val(ch[id][1], rank - siz[ch[id][0]] - cnt[id]); }找前驅和后繼
類似上面的查找
int get_pre(int v) {int id = root, pre;while (id){if (val[id] < v)pre = val[id], id = ch[id][1];elseid = ch[id][0];}return pre; } int get_next(int v) {int id = root, nxt;while (id){if (val[id] > v)nxt = val[id], id = ch[id][0];elseid = ch[id][1];}return nxt; }代碼實現
#include <bits/stdc++.h> using namespace std;int read() {int out = 0, flag = 1;char c = getchar();while (c < '0' || c > '9'){if (c == '-')flag = -1;c = getchar();}while (c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}return flag * out; }const int maxn = 1000019, INF = 1e9; int m; int ch[maxn][2]; int val[maxn], dat[maxn]; int siz[maxn], cnt[maxn]; int tot, root; int New(int v) {val[++tot] = v;dat[tot] = rand();siz[tot] = 1;cnt[tot] = 1;return tot; } void pushup(int id) {siz[id] = siz[ch[id][0]] + siz[ch[id][1]] + cnt[id]; } void Rotate(int &id, int d) {int temp = ch[id][d ^ 1];ch[id][d ^ 1] = ch[temp][d];ch[temp][d] = id;id = temp;pushup(ch[id][d]), pushup(id); } void insert(int &id, int v) {if (!id){id = New(v);return;}if (v == val[id])cnt[id]++;else{int d = v < val[id] ? 0 : 1;insert(ch[id][d], v);if (dat[id] < dat[ch[id][d]])Rotate(id, d ^ 1);}pushup(id); } void Remove(int &id, int v) {if (!id)return;if (v == val[id]){if (cnt[id] > 1){cnt[id]--, pushup(id);return;}if (ch[id][0] || ch[id][1]){if (!ch[id][1] || dat[ch[id][0]] > dat[ch[id][1]]){Rotate(id, 1), Remove(ch[id][1], v);}elseRotate(id, 0), Remove(ch[id][0], v);pushup(id);}elseid = 0;return;}v < val[id] ? Remove(ch[id][0], v) : Remove(ch[id][1], v);pushup(id); } int get_rank(int id, int v) {if (!id)return 0;if (v == val[id])return siz[ch[id][0]] + 1;else if (v < val[id])return get_rank(ch[id][0], v);elsereturn siz[ch[id][0]] + cnt[id] + get_rank(ch[id][1], v); } int get_val(int id, int rank) {if (!id)return INF;if (rank <= siz[ch[id][0]])return get_val(ch[id][0], rank);else if (rank <= siz[ch[id][0]] + cnt[id])return val[id];elsereturn get_val(ch[id][1], rank - siz[ch[id][0]] - cnt[id]); } int get_pre(int v) {int id = root, pre;while (id){if (val[id] < v)pre = val[id], id = ch[id][1];elseid = ch[id][0];}return pre; } int get_next(int v) {int id = root, nxt;while (id){if (val[id] > v)nxt = val[id], id = ch[id][0];elseid = ch[id][1];}return nxt; } int main() {m = read();for (int i = 1; i <= m; i++){int cmd = read(), x = read();if (cmd == 1)insert(root, x);else if (cmd == 2)Remove(root, x);else if (cmd == 3)printf("%d\n", get_rank(root, x));else if (cmd == 4)printf("%d\n", get_val(root, x));else if (cmd == 5)printf("%d\n", get_pre(x));else if (cmd == 6)printf("%d\n", get_next(x));}return 0; }fhq Treap
F H Q T r e a p FHQ \ Treap FHQ?Treap 好理解,上手快,代碼一般很短,支持可持久化,可以實現 T r e a p Treap Treap 的功能,并且不需要 T r e a p Treap Treap 的旋轉操作,所以 F H Q T r e a p FHQ \ Treap FHQ?Treap 又被稱為 無旋 T r e a p Treap Treap 或者 非旋 T r e a p Treap Treap。
算法思想
無旋 T r e a p Treap Treap 的主要操作有 分裂( s p l i t split split )和合并( m e r g e merge merge )兩種。
顧名思義,無旋 T r e a p Treap Treap 保持樹平衡的方式就是不斷地將樹按照某種方式分裂成兩棵子樹,再通過合并子樹來調整節點的祖孫關系。而分裂又分為 按值分裂 和 按大小分裂 兩種,通常情況下,我們會選擇按值分裂。
無旋 T r e a p Treap Treap 和 T r e a p Treap Treap 一樣,給每個節點都附上一個新的隨機權值 k e y key key。
同樣地,原本的點權滿足二叉搜索樹的性質,隨機權值滿足堆的性質。
分裂
S p l i t Split Split 的意思就是將這顆二叉樹按某種條件掰開兩半。
我們將分裂后左邊的樹定義為 X X X,右邊的樹定義為 Y Y Y,它們的根為 x x x 和 y y y。
若以 v a l val val分裂,則其中以 x x x 為根的子樹滿足所有節點的權值都小于等于 v a l val val,以 y y y 為根的子樹滿足所有節點的權值都大于 v a l val val,這就是按值分裂的規則。
假如一棵樹要以 6 6 6 來掰開,如圖 :
然后大力一掰 ? ? ?
具體的算法流程也很簡單。
假如 r o o t = 0 root=0 root=0,說明當前子樹為空樹,無法分裂,所以令 x = y = 0 x=y=0 x=y=0。
若 r o o t root root 的權值小于等于 v a l val val,說明 r o o t root root 應該劃分在以 x x x 為根的子樹內。
因為無旋 T r e a p Treap Treap 是一棵二叉搜索樹,所以 r o o t root root 的左子樹中任意一點的權值 ≤ r o o t ≤root ≤root 的權值 ≤ v a l ≤val ≤val, r o o t root root 的左子樹也應該劃分入以 x x x 為根的子樹。
此時右子樹里可能會出現點權比 v a l val val 大的節點,所以在右子樹內繼續遞歸分裂。
r o o t root root 的權值大于 v a l val val 的情況同理,將 r o o t root root 及其右子樹劃分入以 y y y 為根的子樹,繼續在 r o o t root root 的左子樹內查找即可。
void split(int rt, int val, int &x, int &y) {if (!rt){x = y = 0;return;}if (tr[rt].val <= val){x = rt;split(tr[rt].r, val, tr[rt].r, y);}else{y = rt;split(tr[rt].l, val, x, tr[rt].l);}update(rt); }合并
合并操作指將以 x x x 為根的子樹和以 y y y 為根的子樹合并成一整棵樹,并返回新樹根節點的下標。
合并得到的新樹滿足無旋 T r e a p Treap Treap 的性質,同時要求以 x x x 為根的子樹中,所有節點的權值必須小于等于以 y y y 為根的子樹中任意一點的權值。
假如 x x x 和 y y y 中存在至少一個 0,那么相當于其中 0 0 0 棵或 1 1 1 棵子樹構成了合并出來的新樹,此時直接返回 x+y 即可。
若 x x x 的隨機權值大于 y y y 的隨機權值,說明 x x x 必須是 y y y 的父節點。
又因為 y y y 的點權大于 x x x 的點權,所以 y y y 必須是 x x x 的右兒子,將 x x x 的右兒子與以 y y y 為根的子樹合并即可。
若 x x x 的隨機權值小于等于 y y y 的隨機權值,說明 x x x 必須是 y y y 的左兒子,將以 x x x 為根的子樹與 y y y 的左兒子合并即可。
int merge(int x, int y) {if (!x || !y){return x + y;}if (tr[x].key > tr[y].key){tr[x].r = merge(tr[x].r, y);update(x);return x;}else{tr[y].l = merge(x, tr[y].l);update(y);return y;} }更新節點大小
與 T r e a p Treap Treap 不同的是,無旋 T r e a p Treap Treap 中點權相同的節點個數僅統計一次。
void update(int k) {tr[k].size = tr[tr[k].l].size + tr[tr[k].r].size + 1; }新建節點
int New(int v) {tr[++cnt].val = v;tr[cnt].size = 1;tr[cnt].key = rand();return cnt; }插入
插入一個權值為 v a l val val 的節點,直接將整棵樹按 v a l val val 分裂成兩棵以 x x x, y y y 為根的子樹,令新建節點的下標為 z z z ,此時按順序合并 x,z,y 即可。
void insert(int val) {z = New(val);split(root, val, x, y);root = merge(merge(x, z), y); }刪除
直接將整棵樹按 v a l val val 分裂兩棵以 x , z x,z x,z 為根的子樹。
再將以 x x x 為根的子樹按 v a l ? 1 val?1 val?1 分裂成兩棵以 x , y x,y x,y 為根的子樹。
此時,以 x x x 為根的子樹內所有點權均小于等于 v a l ? 1 val?1 val?1,也就是說,點權等于 v a l val val 的節點都被劃分到了以 y y y 為根的子樹內。
此時在以 y y y 為根的子樹內任意刪除一個節點(通常選擇根節點)即可,具體實現可以直接令以 y y y 為根的子樹為其左子樹和右子樹合并得到的樹,比原樹恰好少了一個根節點。
void del(int val) {split(root, val, x, z);split(x, val - 1, x, y);y = merge(tr[y].l, tr[y].r);root = merge(merge(x, y), z); }查詢排名
整棵樹按 v a l ? 1 val?1 val?1 分裂成兩棵以 x x x, y y y 為根的子樹。
此時以 x x x 為根的子樹中任意點權小于 v a l val val,所以答案就是以 x x x 為根的子樹的大小 + 1 +1 +1。
int getrank(int val) {split(root, val - 1, x, y);int ret = tr[x].size + 1;root = merge(x, y);return ret; }查詢數值
從根節點開始查找,
-
如果左子樹的大小 + 1 = r k +1=rk +1=rk ,說明當前節點就是要查找的數值,直接退出;
-
如果左子樹的大小 ≥ r k ≥rk ≥rk,說明要查找的數一定在左子樹中,在左子樹內繼續查找;
-
否則,要查找的樹一定是右子樹中排名為 r k ? 左 子 樹 大 小 ? 1 rk? 左子樹大小 ?1 rk?左子樹大小?1 的數。
查找前驅
將整棵樹按 v a l ? 1 val?1 val?1 分裂成兩棵以 x x x, y y y 為根的子樹。
此時以 x x x 為根的子樹內所有點權一定都小于 v a l val val,查找以 x x x 為根的子樹內最大的點權即可。
從 x x x 開始,不斷地走到右兒子,直到走到葉子節點為止。
int pre(int v) {// 方法1split(root, v - 1, x, y);int rt = x;while (tr[rt].r)rt = tr[rt].r;root = merge(x, y);return tr[rt].val;// 方法2// return getval(getrank(v) - 1); }查找后繼
將整棵樹按 v a l val val 分裂成兩棵以 x x x, y y y 為根的子樹。
此時以 y y y 為根的子樹內所有點權一定都大于 v a l val val,查找以 y y y 為根的子樹內最小的點權即可。
從 y y y 開始,不斷地走到左兒子,直到走到葉子節點為止。
int nxt(int v) {// 方法1split(root, v, x, y);int rt = y;while (tr[rt].l)rt = tr[rt].l;root = merge(x, y);return tr[rt].val;// 方法2// return getval(getrank(v + 1)); }代碼實現
#include <bits/stdc++.h> using namespace std; #define _ (int)1e5 + 7int n;int root;int cnt;int x, y, z;struct Tree {int l, r, key, val, size; } tr[_];void update(int k) {tr[k].size = tr[tr[k].l].size + tr[tr[k].r].size + 1; }int New(int v) {tr[++cnt].val = v;tr[cnt].size = 1;tr[cnt].key = rand();return cnt; }void split(int rt, int val, int &x, int &y) {if (!rt){x = y = 0;return;}if (tr[rt].val <= val){x = rt;split(tr[rt].r, val, tr[rt].r, y);}else{y = rt;split(tr[rt].l, val, x, tr[rt].l);}update(rt); }int merge(int x, int y) {if (!x || !y){return x + y;}if (tr[x].key > tr[y].key){tr[x].r = merge(tr[x].r, y);update(x);return x;}else{tr[y].l = merge(x, tr[y].l);update(y);return y;} }void insert(int val) {z = New(val);split(root, val, x, y);root = merge(merge(x, z), y); }void del(int val) {split(root, val, x, z);split(x, val - 1, x, y);y = merge(tr[y].l, tr[y].r);root = merge(merge(x, y), z); }int getrank(int val) {split(root, val - 1, x, y);int ret = tr[x].size + 1;root = merge(x, y);return ret; }int getval(int rk) {int rt = root;while (rt){if (tr[tr[rt].l].size + 1 == rk){break;}if (tr[tr[rt].l].size >= rk){rt = tr[rt].l;}else{rk -= (tr[tr[rt].l].size + 1);rt = tr[rt].r;}}return tr[rt].val; }int pre(int v) {// 方法1split(root, v - 1, x, y);int rt = x;while (tr[rt].r)rt = tr[rt].r;root = merge(x, y);return tr[rt].val;// 方法2// return getval(getrank(v) - 1); }int nxt(int v) {// 方法1split(root, v, x, y);int rt = y;while (tr[rt].l)rt = tr[rt].l;root = merge(x, y);return tr[rt].val;// 方法2// return getval(getrank(v + 1)); }signed main() {scanf("%d", &n);for (int i = 1; i <= n; ++i){int opt, x;scanf("%d%d", &opt, &x);if (opt == 1){insert(x);}else if (opt == 2){del(x);}else if (opt == 3){printf("%d\n", getrank(x));}else if (opt == 4){printf("%d\n", getval(x));}else if (opt == 5){printf("%d\n", pre(x));}else{printf("%d\n", nxt(x));}} }對于樹中的任意一個節點,滿足 :該節點的關鍵碼不小于它的左子樹中任意節點的關鍵碼且不大于也不等于它的右子樹中任意節點的關鍵碼。 ?? ?? ?? ?? ?? ??
堆中每一個節點的值都必須大于等于(或小于等于)其子樹中每個節點的值。 ?? ??
總結
- 上一篇: 1096 大美数 (15 分)
- 下一篇: Qt程序移植