// Return a new empty node.int new_node() { int x = top ? pool[--top] : ++id; sz[x] = val[x] = ch[x][0] = ch[x][1] = 0; return x;}// Release a node for later use.void del_node(int& x) { pool[top++] = x; x = 0;}// Return a new leaf node of value v.int new_leaf(int v) { int x = new_node(); val[x] = v; sz[x] = 1; return x;}// Return a new node with subtrees x and y.int join(int x, int y) { int z = new_node(); ch[z][0] = x; ch[z][1] = y; push_up(z); return z;}// Return subtrees of x and release x.auto cut(int& x) { int y = ch[x][0]; int z = ch[x][1]; del_node(x); return std::make_pair(y, z);}
封装好这些辅助函数后,数组实现和指针实现在后续函数中就没有区别了。
平衡的概念
对于一个树,可以定义它在一个非叶节点 x 处的 平衡度 为
ρ(x)=w(Tx)min{w(Tleft(x)),w(Tright(x))}.
其中,Tx 表示以 x 为根的子树,w(⋅) 表示子树的权重(它的叶子节点的数目),而 left(x) 和 right(x) 分别表示 x 的左右叶子节点。特别地,叶子节点处规定 ρ(x)=1/2。
// Rotate the subtree at x such that ch[x][r] is the new root.void rotate(int& x, bool r) { int y = ch[x][r]; ch[x][r] = ch[y][!r]; ch[y][!r] = x; push_up(x); push_up(y); x = y;}
依赖连接
// Rotate the subtree at x such that ch[x][r] is the new root.void rotate(int& x, bool r) { int a, b, c, d; std::tie(a, b) = cut(x); if (r) { std::tie(c, d) = cut(b); x = join(join(a, c), d); } else { std::tie(c, d) = cut(a); x = join(c, join(d, b)); }}
假设在某个树的修改操作后,正在自下而上地恢复树的平衡。现在,左右子树 x 和 y 不再平衡,但是它们自身都是平衡的。不妨设右子树 y 过轻,即 y<α(x+y)。此时,树的形态如图中左侧的树所示。
一种朴素的平衡维护策略是将 x 旋转到根节点处,这样它原先的右子节点 w 就和 y 一起成为了新树的右子节点,而它原先的左子节点 z 成为了新树的左子节点。这相当于将原来的树左侧中 w 的权重移动到它的右侧。如果 w 的权重合适,这样的操作就可以恢复树的平衡。这样得到的树如图中右侧的树所示。
但是,如果 w 本身过重,这样的操作可能移动了太多的权重到右子树,从而使得新树中左子树过轻,即 z<α(x+y)。对于这种情形,因为子树 z 和子树 y 的权重都太小,只能考虑将 w 分拆为两个子树,分别与 z 和 y 连接,成为新树的两个子树。这相当于首先将节点 w 旋转到节点 x 处,再将它旋转到根节点处。同样,可以期待这样得到的树能够达到平衡,形态如图中上方的树所示。
这两种旋转的策略分别称为单旋和双旋。单旋和双旋策略的选取,主要取决于子树 w 相对于子树 x 的比重,即存在阈值 β,使得
// Check whether a subtree of weight SX is too heavy// in a tree of weight SX + SY.bool too_heavy(int sx, int sy) { // or sx > sy * 3; return sy < ALPHA * (sx + sy);}// Check if ch[x][!r] is too heavy so that a double rotation is needed.bool need_double_rotation(int x, bool r) { // or sz[ch[x][!r]] > sz[ch[x][r]] * 2; return sz[ch[x][!r]] > sz[x] / (2 - ALPHA);}// Balance the subtree at x;void balance(int& x) { if (sz[x] == 1) return; bool r = sz[ch[x][1]] > sz[ch[x][0]]; if (!too_heavy(sz[ch[x][r]], sz[ch[x][!r]])) return; if (need_double_rotation(ch[x][r], r)) { rotate(ch[x][r], !r); } rotate(x, r);}
实现了维护平衡的策略后,合并两树的算法就非常简单。仍然设 x>y,合并的策略如下:
如果右子树 y 是空的,直接返回左子树 x;
如果左右子树 x 和 y 已经平衡,即 y≥α(x+y),直接连接两子树;
否则,将 x 的右子树 w 与 y 合并,将左子树 z 与它们合并的结果连接,并调整新树的平衡。
参考实现如下:
参考代码
// Merge two subtrees.int merge(int x, int y) { if (!x || !y) return x | y; int a, b; if (too_heavy(sz[x], sz[y])) { std::tie(a, b) = cut(x); int z = join(a, merge(b, y)); balance(z); return z; } else if (too_heavy(sz[y], sz[x])) { std::tie(a, b) = cut(y); int z = join(merge(x, a), b); balance(z); return z; } else { return join(x, y); }}
可以证明,这样可以维持合并后树的平衡,且这样操作的复杂度是 O(∣log(x/y)∣) 的。
平衡和复杂度的证明
只需要考虑 y 过轻的情形,即 y<α(x+y)。此时,先合并 w 和 y,再连接 z 和 w+y。需要证明的是,只要在树根处调整树的平衡,就能够保证树的平衡。假设树 w+y 的左、右子树分别是 c 和 d,且 c 的左、右子树分别是 a 和 b。在树根处调整平衡,可以分为三种情形:
情形一: z 和 w+y 已经平衡,无需进一步调整,即 z≥α(x+y)
根据平衡的定义,子树 z 和 w+y 都是平衡的,且它们互相也是平衡的,那么整棵树也是平衡的。
情形二: z 过轻且 c 没有过重,可以通过单旋恢复平衡,即 z<α(x+y) 且 c≤β(w+y)
此时,因为 z 和 w 平衡,但 y 相较于 x=z+w 过轻,所以,子树 z 的权重满足
最后,简单说明一下该算法的复杂度为什么是 O(∣log(x/y)∣) 的。合并的流程中,如果 y 相较于 x 过轻,就尝试与 x 的右子树合并,这个过程一直持续到以 x 某个子孙节点为根的子树与 y 平衡为止。因为每向下加深一层,子树权重至少变为原来的 (1−α),所以至多只要 log1−α1(x/y) 次迭代,就能找到与 y 平衡的子树。因此,该合并算法调用 O(logn) 次平衡算法 2,复杂度也就是 O(logn)。
虽然并不明显,但是这个论证过程依赖于这样一个结论:不断取右子树的过程中,y 不会在一次迭代前后,从相较于左侧的子树过轻,变为相较于它过重。这是因为能够与 y 平衡的子树权重范围位于 αy/(1−α) 与 (1−α)y/α 之间。因此,如果在一次迭代时,就从 y 过轻变成 y 过重,则 x 的子树的权重在该次迭代过程中至少缩小到了原来的 α2/(1−α)2 倍。但是,单次迭代,子树权重至多只能缩小到原来的 α 倍,但是在上述 α 的范围中,α>α2/(1−α)2。这说明,前设情形是不可能的,某次迭代之后一定会有 y 与 x 的某个子树平衡的情形发生。
// Merge two subtrees.int merge(int x, int y) { if (!x || !y) return x | y; int a, b, c, d; if (too_heavy(sz[x], sz[y])) { std::tie(a, b) = cut(x); if (too_heavy(sz[b] + sz[y], sz[a])) { std::tie(c, d) = cut(b); return merge(merge(a, c), merge(d, y)); } else { return merge(a, merge(b, y)); } } else if (too_heavy(sz[y], sz[x])) { std::tie(a, b) = cut(y); if (too_heavy(sz[a] + sz[x], sz[b])) { std::tie(c, d) = cut(a); return merge(merge(x, c), merge(d, b)); } else { return merge(merge(x, a), b); } } else { return join(x, y); }}
利用该合并策略,同样可以很容易实现树的平衡维护:失衡时,直接合并左右子树即可。
参考代码
// Balance the subtree at x;void balance(int& x) { if (sz[x] == 1) return; if (too_heavy(sz[ch[x][0]], sz[ch[x][1]]) || too_heavy(sz[ch[x][1]], sz[ch[x][0]])) { int a, b; std::tie(a, b) = cut(x); x = merge(a, b); }}
// Build the tree for the interval [ll, rr].int build(int ll, int rr) { if (ll == rr) return new_leaf(ll); int mm = (ll + rr) / 2; return join(build(ll, mm), build(mm + 1, rr));}
// Count the number of nodes less than v in the subtree at x.int count_less_than(int x, int v) { if (!x) return 0; int res = 0; while (sz[x] > 1) { if (val[ch[x][0]] < v) { res += sz[ch[x][0]]; x = ch[x][1]; } else { x = ch[x][0]; } } return res + (val[x] < v ? 1 : 0);}// Find the rank of v.int find_rank(int v) { return count_less_than(rt, v) + 1; }
时间复杂度为 O(logn)。
根据排名查询
依然是利用线段树上二分的思想,只不过这里比较的是节点的权重。
参考实现如下:
参考代码
// Find the k-th element in the subtree at x.// It is guaranteed that such an element exists.int find_kth(int x, int k) { while (sz[x] > 1) { if (sz[ch[x][0]] >= k) { x = ch[x][0]; } else { k -= sz[ch[x][0]]; x = ch[x][1]; } } return val[x];}// Find the k-th element.int find_kth(int k) { return k > sz[rt] || k <= 0 ? -1 : find_kth(rt, k); }
时间复杂度为 O(logn)。
查找前驱、后继
以上两种功能结合即可。
参考实现如下:
参考代码
// Find the predecessor of v.int find_prev(int v) { return find_kth(find_rank(v) - 1); }// Find the successor of v.int find_next(int v) { return find_kth(find_rank(v + 1)); }
// Split the subtree at x.// The left half will have k elements.std::pair<int, int> split(int x, int k) { if (!x) return {0, 0}; if (!k) return {0, x}; if (k == sz[x]) return {x, 0}; int a, b; std::tie(a, b) = cut(x); if (k <= sz[a]) { int ll, rr; std::tie(ll, rr) = split(a, k); return {ll, merge(rr, b)}; } else { int ll, rr; std::tie(ll, rr) = split(b, k - sz[a]); return {merge(a, ll), rr}; }}
#include <iostream>#include <tuple>// Change here to use different strategies to balance and to merge.#define BALANCE_BY_ROTATING 1#define ROTATE_BY_JOINING 0constexpr int N = 2e6;constexpr double ALPHA = 0.292;int id, rt, ch[N][2], sz[N], val[N];int pool[N], top;// --8<-- [start:push-up]void push_up(int x) { sz[x] = sz[ch[x][0]] + sz[ch[x][1]]; val[x] = val[ch[x][1]];}// --8<-- [end:push-up]// --8<-- [start:helper]// Return a new empty node.int new_node() { int x = top ? pool[--top] : ++id; sz[x] = val[x] = ch[x][0] = ch[x][1] = 0; return x;}// Release a node for later use.void del_node(int& x) { pool[top++] = x; x = 0;}// Return a new leaf node of value v.int new_leaf(int v) { int x = new_node(); val[x] = v; sz[x] = 1; return x;}// Return a new node with subtrees x and y.int join(int x, int y) { int z = new_node(); ch[z][0] = x; ch[z][1] = y; push_up(z); return z;}// Return subtrees of x and release x.auto cut(int& x) { int y = ch[x][0]; int z = ch[x][1]; del_node(x); return std::make_pair(y, z);}// --8<-- [end:helper]// --8<-- [start:too-heavy]// Check whether a subtree of weight SX is too heavy// in a tree of weight SX + SY.bool too_heavy(int sx, int sy) { // or sx > sy * 3; return sy < ALPHA * (sx + sy);}// --8<-- [end:too-heavy]#if BALANCE_BY_ROTATING#if ROTATE_BY_JOINING// --8<-- [start:rotate-by-joining]// Rotate the subtree at x such that ch[x][r] is the new root.void rotate(int& x, bool r) { int a, b, c, d; std::tie(a, b) = cut(x); if (r) { std::tie(c, d) = cut(b); x = join(join(a, c), d); } else { std::tie(c, d) = cut(a); x = join(c, join(d, b)); }}// --8<-- [end:rotate-by-joining]#else// --8<-- [start:rotate-not-by-joining]// Rotate the subtree at x such that ch[x][r] is the new root.void rotate(int& x, bool r) { int y = ch[x][r]; ch[x][r] = ch[y][!r]; ch[y][!r] = x; push_up(x); push_up(y); x = y;}// --8<-- [end:rotate-not-by-joining]#endif// --8<-- [start:balance]// Check if ch[x][!r] is too heavy so that a double rotation is needed.bool need_double_rotation(int x, bool r) { // or sz[ch[x][!r]] > sz[ch[x][r]] * 2; return sz[ch[x][!r]] > sz[x] / (2 - ALPHA);}// Balance the subtree at x;void balance(int& x) { if (sz[x] == 1) return; bool r = sz[ch[x][1]] > sz[ch[x][0]]; if (!too_heavy(sz[ch[x][r]], sz[ch[x][!r]])) return; if (need_double_rotation(ch[x][r], r)) { rotate(ch[x][r], !r); } rotate(x, r);}// --8<-- [end:balance]// --8<-- [start:merge-by-balancing]// Merge two subtrees.int merge(int x, int y) { if (!x || !y) return x | y; int a, b; if (too_heavy(sz[x], sz[y])) { std::tie(a, b) = cut(x); int z = join(a, merge(b, y)); balance(z); return z; } else if (too_heavy(sz[y], sz[x])) { std::tie(a, b) = cut(y); int z = join(merge(x, a), b); balance(z); return z; } else { return join(x, y); }}// --8<-- [end:merge-by-balancing]#else// --8<-- [start:merge]// Merge two subtrees.int merge(int x, int y) { if (!x || !y) return x | y; int a, b, c, d; if (too_heavy(sz[x], sz[y])) { std::tie(a, b) = cut(x); if (too_heavy(sz[b] + sz[y], sz[a])) { std::tie(c, d) = cut(b); return merge(merge(a, c), merge(d, y)); } else { return merge(a, merge(b, y)); } } else if (too_heavy(sz[y], sz[x])) { std::tie(a, b) = cut(y); if (too_heavy(sz[a] + sz[x], sz[b])) { std::tie(c, d) = cut(a); return merge(merge(x, c), merge(d, b)); } else { return merge(merge(x, a), b); } } else { return join(x, y); }}// --8<-- [end:merge]// --8<-- [start:balance-by-merging]// Balance the subtree at x;void balance(int& x) { if (sz[x] == 1) return; if (too_heavy(sz[ch[x][0]], sz[ch[x][1]]) || too_heavy(sz[ch[x][1]], sz[ch[x][0]])) { int a, b; std::tie(a, b) = cut(x); x = merge(a, b); }}// --8<-- [end:balance-by-merging]#endif// --8<-- [start:insert-remove]// Insert v to the subtree at x.void insert(int& x, int v) { if (!x) { x = new_leaf(v); } else if (sz[x] == 1) { bool r = v >= val[x]; ch[x][r] = new_leaf(v); ch[x][!r] = new_leaf(val[x]); push_up(x); } else { bool r = v > val[ch[x][0]]; insert(ch[x][r], v); push_up(x); balance(x); }}// Insert v.void insert(int v) { insert(rt, v); }// Remove v from the subtree at x.bool remove(int& x, int v) { if (!x) return false; if (sz[x] == 1) { if (val[x] == v) { del_node(x); return true; } else { return false; } } else { bool r = v > val[ch[x][0]]; bool succ = remove(ch[x][r], v); if (!ch[x][r]) { x = ch[x][!r]; } else { push_up(x); balance(x); } return succ; }}// Remove v.bool remove(int v) { return remove(rt, v); }// --8<-- [end:insert-remove]// --8<-- [start:rank]// Count the number of nodes less than v in the subtree at x.int count_less_than(int x, int v) { if (!x) return 0; int res = 0; while (sz[x] > 1) { if (val[ch[x][0]] < v) { res += sz[ch[x][0]]; x = ch[x][1]; } else { x = ch[x][0]; } } return res + (val[x] < v ? 1 : 0);}// Find the rank of v.int find_rank(int v) { return count_less_than(rt, v) + 1; }// --8<-- [end:rank]// --8<-- [start:kth-element]// Find the k-th element in the subtree at x.// It is guaranteed that such an element exists.int find_kth(int x, int k) { while (sz[x] > 1) { if (sz[ch[x][0]] >= k) { x = ch[x][0]; } else { k -= sz[ch[x][0]]; x = ch[x][1]; } } return val[x];}// Find the k-th element.int find_kth(int k) { return k > sz[rt] || k <= 0 ? -1 : find_kth(rt, k); }// --8<-- [end:kth-element]// --8<-- [start:prev-next]// Find the predecessor of v.int find_prev(int v) { return find_kth(find_rank(v) - 1); }// Find the successor of v.int find_next(int v) { return find_kth(find_rank(v + 1)); }// --8<-- [end:prev-next]int main() { int n; std::cin >> n; for (; n; --n) { int op, x; std::cin >> op >> x; switch (op) { case 1: insert(x); break; case 2: remove(x); break; case 3: std::cout << find_rank(x) << '\n'; break; case 4: std::cout << find_kth(x) << '\n'; break; case 5: std::cout << find_prev(x) << '\n'; break; case 6: std::cout << find_next(x) << '\n'; break; } } return 0;}
#include <iostream>#include <tuple>// Change here to use different strategies to balance and to merge.#define BALANCE_BY_ROTATING 0#define ROTATE_BY_JOINING 1constexpr int N = 2e5;constexpr double ALPHA = 0.292;int id, rt, ch[N][2], sz[N], val[N], lz[N];int pool[N], top;void push_up(int x) { sz[x] = sz[ch[x][0]] + sz[ch[x][1]]; }void lazy_reverse(int x) { if (!x) return; std::swap(ch[x][0], ch[x][1]); lz[x] ^= 1;}void push_down(int x) { if (lz[x]) { lazy_reverse(ch[x][0]); lazy_reverse(ch[x][1]); lz[x] = 0; }}// Return a new empty node.int new_node() { int x = top ? pool[--top] : ++id; sz[x] = val[x] = ch[x][0] = ch[x][1] = 0; return x;}// Release a node for later use.void del_node(int& x) { pool[top++] = x; x = 0;}// Return a new leaf node of value v.int new_leaf(int v) { int x = new_node(); val[x] = v; sz[x] = 1; return x;}// Return a new node with subtrees x and y.int join(int x, int y) { int z = new_node(); ch[z][0] = x; ch[z][1] = y; push_up(z); return z;}// Return subtrees of x and release x.auto cut(int& x) { push_down(x); int y = ch[x][0]; int z = ch[x][1]; del_node(x); return std::make_pair(y, z);}// Check whether a subtree of weight SX is too heavy// in a tree of weight SX + SY.bool too_heavy(int sx, int sy) { // or sx > sy * 3; return sy < ALPHA * (sx + sy);}#if BALANCE_BY_ROTATING#if ROTATE_BY_JOINING// Rotate the subtree at x such that ch[x][r] is the new root.void rotate(int& x, bool r) { int a, b, c, d; std::tie(a, b) = cut(x); if (r) { std::tie(c, d) = cut(b); x = join(join(a, c), d); } else { std::tie(c, d) = cut(a); x = join(c, join(d, b)); }}#else// Rotate the subtree at x such that ch[x][r] is the new root.void rotate(int& x, bool r) { int y = ch[x][r]; ch[x][r] = ch[y][!r]; ch[y][!r] = x; push_up(x); push_up(y); x = y;}#endif// Check if ch[x][!r] is too heavy so that a double rotation is needed.bool need_double_rotation(int x, bool r) { // or sz[ch[x][!r]] > sz[ch[x][r]] * 2; return sz[ch[x][!r]] > sz[x] / (2 - ALPHA);}// Balance the subtree at x;void balance(int& x) { if (sz[x] == 1) return; push_down(x); bool r = sz[ch[x][1]] > sz[ch[x][0]]; if (!too_heavy(sz[ch[x][r]], sz[ch[x][!r]])) return; push_down(ch[x][r]); if (need_double_rotation(ch[x][r], r)) { push_down(ch[ch[x][r]][!r]); rotate(ch[x][r], !r); } rotate(x, r);}// Merge two subtrees.int merge(int x, int y) { if (!x || !y) return x | y; int a, b; if (too_heavy(sz[x], sz[y])) { std::tie(a, b) = cut(x); int z = join(a, merge(b, y)); balance(z); return z; } else if (too_heavy(sz[y], sz[x])) { std::tie(a, b) = cut(y); int z = join(merge(x, a), b); balance(z); return z; } else { return join(x, y); }}#else// Merge two subtrees.int merge(int x, int y) { if (!x || !y) return x | y; int a, b, c, d; if (too_heavy(sz[x], sz[y])) { std::tie(a, b) = cut(x); if (too_heavy(sz[b] + sz[y], sz[a])) { std::tie(c, d) = cut(b); return merge(merge(a, c), merge(d, y)); } else { return merge(a, merge(b, y)); } } else if (too_heavy(sz[y], sz[x])) { std::tie(a, b) = cut(y); if (too_heavy(sz[a] + sz[x], sz[b])) { std::tie(c, d) = cut(a); return merge(merge(x, c), merge(d, b)); } else { return merge(merge(x, a), b); } } else { return join(x, y); }}// Balance the subtree at x;void balance(int& x) { if (sz[x] == 1) return; if (too_heavy(sz[ch[x][0]], sz[ch[x][1]]) || too_heavy(sz[ch[x][1]], sz[ch[x][0]])) { int a, b; std::tie(a, b) = cut(x); x = merge(a, b); }}#endif// --8<-- [start:split]// Split the subtree at x.// The left half will have k elements.std::pair<int, int> split(int x, int k) { if (!x) return {0, 0}; if (!k) return {0, x}; if (k == sz[x]) return {x, 0}; int a, b; std::tie(a, b) = cut(x); if (k <= sz[a]) { int ll, rr; std::tie(ll, rr) = split(a, k); return {ll, merge(rr, b)}; } else { int ll, rr; std::tie(ll, rr) = split(b, k - sz[a]); return {merge(a, ll), rr}; }}// --8<-- [end:split]// Reverse the interval [l, r].void reverse(int l, int r) { int ll, rr; std::tie(rt, rr) = split(rt, r); std::tie(ll, rt) = split(rt, l - 1); lazy_reverse(rt); rt = merge(ll, merge(rt, rr));}// Output the subtree at x.void print(int x) { if (sz[x] == 1) { std::cout << val[x] << ' '; } else { push_down(x); print(ch[x][0]); print(ch[x][1]); }}// Output the tree.void print() { print(rt); std::cout << '\n';}// --8<-- [start:build]// Build the tree for the interval [ll, rr].int build(int ll, int rr) { if (ll == rr) return new_leaf(ll); int mm = (ll + rr) / 2; return join(build(ll, mm), build(mm + 1, rr));}// --8<-- [end:build]int main() { int n, m; std::cin >> n >> m; rt = build(1, n); for (; m; --m) { int l, r; std::cin >> l >> r; reverse(l, r); } print(); return 0;}
Nievergelt, J.; Reingold, E. M. (1973). “Binary Search Trees of Bounded Balance”. SIAM Journal on Computing. 2: 33–43.
Blum, Norbert; Mehlhorn, Kurt (1980). “On the average number of rebalancing operations in weight-balanced trees”. Theoretical Computer Science. 11 (3): 303–320.
Hirai, Y.; Yamamoto, K. (2011). “Balancing weight-balanced trees”. Journal of Functional Programming. 21 (3): 287.
Blelloch, Guy E.; Ferizovic, Daniel; Sun, Yihan (2016), “Just Join for Parallel Ordered Sets”, Symposium on Parallel Algorithms and Architectures, Proc. of 28th ACM Symp. Parallel Algorithms and Architectures (SPAA 2016), ACM, pp. 253–264.
Straka, Milan. (2011). “Adams’Trees Revisited: Correctness Proof and Efficient Implementation.” International Symposium on Trends in Functional Programming. Berlin, Heidelberg: Springer Berlin Heidelberg.