平衡树的一些记录和带插入区间K小值
平衡树和带插入区间K小值
高速平衡树
除了 Splay 和无旋 Treap 以外的平衡树都是高速平衡树。忘了出处是哪里了,好像是 shadowice1984。
无旋 Treap 的一些写法
随机权值
Treap 的随机权值可以不维护。假如权值是小根堆,那么点 \(p\) 的随机权值可以认为是 \(siz_p\) 个随机数中的最小值。那么我们在比较两个点的随机权值时,使它有 \(\dfrac{siz_p}{siz_p+siz_q}\) 的概率是 \(p\) 的权值小于 \(q\) 的权值,反之亦然。
插入删除只用一次 split/merge
假设随机值是小根堆,对于插入,我们比较随机值,如果随机值比树根小,就把这个树按照插入权值 split 开,然后当成插入节点的两个儿子。不然我们就根据权值比较,进入对应子树继续插入。
删除我们就找到这个节点,把两个儿子 merge 起来,连上去。
如果你学过旋 treap,这个是基本一样的。这样操作还提供了一些额外的性质:
距离叶子期望距离 \(O(1)\),我们这个节点的期望子树大小 \(O(\log_2 n)\)。
前者保证了我们 split, merge 时候的深度很小,是常数时间,瓶颈在于在上方路径的探查,而这一部分效率非常高。
后者则让我们可以用 treap 实现 平衡树套树,动态标号等功能。
插入的复杂度证明:
对于你要插入的数,你先找到它在 treap 上二分出来的链。我们算每个位置停下的概率乘子树大小的和,就是期望子树大小。
在一个节点停下的概率不好算,但是它不大于在这个节点及上面停下的概率。这个概率就是 \(\frac{1}{1+size}\),\(size\) 是子树大小。
然后就是 \(\sum size\times \frac{1}{size+1}\),就是深度,就是 \(O(\log_2 n)\)。
对于离叶子距离,大家感受一下就好,反正是对的。
这样的写法不改变复杂度,但是却实实在在地优化了常数。
区间查询
例如 Treap 维护一个有序序列,查询值域在某个区间内的数有多少个。可以将区间拆成两个前缀的差进行统计。
当然更好的做法是像线段树一样写。我们思考难点在哪里?难点在于我们不知道当前子树代表的区间范围,导致无法判断递归是否需要停止。我们可以回避这件事,在父节点判断询问区间是否完全包含子树区间。从根出发,先向下找到一个子节点 \(p\) 使得 \(l\leq w_p\leq r\),从这里开始向 \(p\) 的两个儿子分别递归。当然我们不写递归,而是分开两次处理。以 \(p\) 的左儿子为例,从它出发,如果当前点权值 \(\geq l\),则我们把它和它的右子树全部吃掉,然后跑到左子树;反之跑到右子树。这样就解决了这个问题。
以上的代码汇总
w[pid[p]] 就是节点 \(p\) 的权值,\(w\) 是动态标号,下文介绍。
void insert(int id, int &rt) {
pid[++tot] = id, siz[tot] = 1;
int *p = &rt; // 防止把根节点改掉而已
while (*p && rng() % (siz[*p] + 1)) ++siz[*p], p = &ch[*p][w[id] > w[pid[*p]]];
split(*p, w[id], ch[tot][0], ch[tot][1]);
maintain(*p = tot);
}
void erase(int id, int &rt) {
int *p = &rt;
while (pid[*p] != id) --siz[*p], p = &ch[*p][w[id] > w[pid[*p]]];
*p = merge(ch[*p][0], ch[*p][1]);
}
int query(double ql, double qr, int rt) {
while (rt && (ql > w[pid[rt]] || w[pid[rt]] > qr)) rt = ch[rt][w[pid[rt]] <= ql];
int res = (bool)rt; // 防止根是空的
for (int p = ch[rt][0]; p; ) {
if (w[pid[p]] >= ql) res += siz[ch[p][1]] + 1, p = ch[p][0];
else p = ch[p][1];
}
for (int p = ch[rt][1]; p; ) {
if (w[pid[p]] <= qr) res += siz[ch[p][0]] + 1, p = ch[p][1];
else p = ch[p][0];
}
return res;
}
替罪羊树
替罪羊树是一种依靠重构操作维持平衡的重量平衡树。它有实现简单,常数小的优点。核心思想是,当 \(\max(siz_{ls[p]}, siz_{rs[p]})\geq \alpha\cdot siz_p\) 时,将整棵子树拍扁重构,也就是取出它的中序遍历,重新建一棵完全二叉树。\(\alpha\) 是一个 \((0.5,1)\) 之间的常数,一般取 \(\alpha=0.7\)。
替罪羊树的删除一般使用懒惰删除,即直接给节点打一个删除标记。当被删除的点的占比 \(\geq \alpha\) 时,也重构整棵子树。
替罪羊树的复杂度是均摊的。
动态标号法
问题是:维护一个序列,支持:在序列第 \(x\) 个元素前插入元素;删除序列第 \(x\) 个元素;查询某两次操作插入的点在序列中的先后关系(要求 \(O(1)\))。
可以尝试给每个元素分配一个权值 \(w\)。权值是一个外部数组,不存储在平衡树中。假如把 \(m\) 插到 \(l, r\) 之间,则 \(w_m=(w_l+w_r)/2\)。用平衡树维护,把树高控制在 \(O(\log n)\) 以内,权值的精度就可以接受。
平衡树有两种选择:一是替罪羊树,每次重构的时候重新计算权值;二是有旋 Treap,每次旋转的时候重新计算整棵子树的权值,这样的复杂度也是正确的。Treap 一次插入旋转次数期望 \(O(1)\),旋转的节点的子树大小期望 \(O(\log n)\)。
如何击破 P2617 Dynamic Rankings(带修区间 K-th)
如果我们坚持使用 \(\text{poly}\log\) 的算法,那就必然会出现树套树。问题是选择什么树套什么树。树的类型由两部分组成:一部分是结构,例如这棵树是树状数组、线段树、平衡树之类的(原则上能树状数组就树状数组,否则根据空间需求使用线段树或平衡树);另一部分是信息,例如这棵树是维护权值的,或者是维护位置的。
第一种方法:位置树套权值树,选择树状数组套线段树。外层树状数组把区间拆成 \(O(\log n)\) 个线段树,然后在这 \(O(\log n)\) 个线段树上同时进行线段树二分。
第二种方法:权值树套位置树,选择树状数组套线段树。外层树状数组进行树状数组二分,内层线段树就做区间查询的工作。
如何击破 P4278 带插入区间K小值
对于插入操作,使用动态标号法解决。这样以后,我们就使用权值树套位置树的区间 k-th 算法(防止潜在的内层树合并问题),选择树状数组套无旋 Treap。时间复杂度 \(O(n\log ^2n)\),空间复杂度 \(O(n\log n)\)。
\(pid_p\) 存储平衡树节点对应元素的编号,\(w_i\) 存储元素的权值标号。
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
using LL = long long;
constexpr int N = 70010, V = 70001;
mt19937 rng{141234124};
double w[N];
namespace bbalpha {
int ch[N][2], siz[N];
void maintain(int p) { siz[p] = siz[ch[p][0]] + siz[ch[p][1]] + 1; }
int build(int *fir, int *lst, double l, double r) {
if (fir == lst) return 0;
assert(fir < lst);
auto mid = fir + (lst - fir) / 2;
int p = *mid;
w[p] = (l + r) / 2;
ch[p][0] = build(fir, mid, l, w[p]);
ch[p][1] = build(mid + 1, lst, w[p], r);
maintain(p);
return p;
}
void rebuild(int &p, double l, double r) {
vector<int> vec;
auto dfs = [&](auto self, int p) -> void {
if (!p) return ;
self(self, ch[p][0]);
vec.push_back(p);
self(self, ch[p][1]);
};
dfs(dfs, p);
p = build(vec.data(), vec.data() + vec.size(), l, r);
}
void insert(int x, int k, int &p, double l, double r) {
if (!p) return assert(!k), w[p = x] = (l + r) / 2, siz[p] = 1, void();
if (siz[ch[p][0]] + 1 <= k) insert(x, k - siz[ch[p][0]] - 1, ch[p][1], w[p], r);
else insert(x, k, ch[p][0], l, w[p]);
maintain(p);
if (max(siz[ch[p][0]], siz[ch[p][1]]) >= 0.7 * siz[p]) rebuild(p, l, r);
}
int query(int k, int p) {
assert(p);
if (k == siz[ch[p][0]] + 1) return p;
if (siz[ch[p][0]] + 1 < k) return query(k - siz[ch[p][0]] - 1, ch[p][1]);
return query(k, ch[p][0]);
}
};
namespace treap {
int rt[N], ch[N * 80][2], siz[N * 80], pid[N * 80], tot;
void maintain(int p) {
siz[p] = siz[ch[p][0]] + siz[ch[p][1]] + 1;
}
int merge(int p, int q) {
if (!p || !q) return p + q;
if (rng() % (siz[p] + siz[q]) < siz[p]) {
ch[p][1] = merge(ch[p][1], q);
maintain(p);
return p;
} else {
ch[q][0] = merge(p, ch[q][0]);
maintain(q);
return q;
}
}
void split(int p, double k, int &x, int &y) {
if (!p) return x = y = 0, void();
if (w[pid[p]] <= k) x = p, split(ch[p][1], k, ch[p][1], y);
else y = p, split(ch[p][0], k, x, ch[p][0]);
maintain(p);
}
void split2(int p, double k, int &x, int &y) {
if (!p) return x = y = 0, void();
if (w[pid[p]] < k) x = p, split2(ch[p][1], k, ch[p][1], y);
else y = p, split2(ch[p][0], k, x, ch[p][0]);
maintain(p);
}
void insert(int id, int &rt) {
pid[++tot] = id, siz[tot] = 1;
int *p = &rt; // 防止把根节点改掉而已
while (*p && rng() % (siz[*p] + 1)) ++siz[*p], p = &ch[*p][w[id] > w[pid[*p]]];
split(*p, w[id], ch[tot][0], ch[tot][1]);
maintain(*p = tot);
}
void erase(int id, int &rt) {
int *p = &rt;
while (pid[*p] != id) --siz[*p], p = &ch[*p][w[id] > w[pid[*p]]];
*p = merge(ch[*p][0], ch[*p][1]);
}
int query(double ql, double qr, int rt) {
while (rt && (ql > w[pid[rt]] || w[pid[rt]] > qr)) rt = ch[rt][w[pid[rt]] <= ql];
int res = (bool)rt; // 防止根是空的
for (int p = ch[rt][0]; p; ) {
if (w[pid[p]] >= ql) res += siz[ch[p][1]] + 1, p = ch[p][0];
else p = ch[p][1];
}
for (int p = ch[rt][1]; p; ) {
if (w[pid[p]] <= qr) res += siz[ch[p][0]] + 1, p = ch[p][1];
else p = ch[p][0];
}
return res;
}
};
int n;
void insert(int x, int y) {
debug("insert(%d, %d) [w = %.3lf]\n", x, y, w[x]);
for (int p = y; p <= V; p += p & -p) treap::insert(x, treap::rt[p]);
}
void erase(int x, int y) {
debug("erase(%d, %d) [w = %.3lf]\n", x, y, w[x]);
for (int p = y; p <= V; p += p & -p) treap::erase(x, treap::rt[p]);
}
int query(int l, int r, int k) {
debug("query(%d, %d, %d) [w = [%.3lf, %.3lf]]\n", l, r, k, w[l], w[r]);
int p = 0;
for (int j = 17; j >= 0; j--) {
int q = p | 1 << j, res;
if (q > V) continue;
if (q <= V && (res = treap::query(w[l], w[r], treap::rt[q])) < k) k -= res, p = q;
}
return p;
}
int main() {
#ifndef LOCAL
cin.tie(nullptr)->sync_with_stdio(false);
#endif
cin >> n;
vector<int> vec(n);
iota(vec.begin(), vec.end(), 1);
int rt = bbalpha::build(vec.data(), vec.data() + vec.size(), 0, 1e9);
static int val[N];
for (int i = 1; i <= n; i++) cin >> val[i], insert(i, ++val[i]);
int q, lstans = 0;
cin >> q;
while (q--) {
char op;
int x, y, k;
cin >> op >> x >> y;
x ^= lstans, y ^= lstans;
if (op == 'Q') cin >> k, k ^= lstans, cout << (lstans = query(bbalpha::query(x, rt), bbalpha::query(y, rt), k)) << endl;
else if (op == 'M') x = bbalpha::query(x, rt), erase(x, val[x]), insert(x, val[x] = ++y);
else bbalpha::insert(++n, --x, rt, 0, 1e9), insert(n, val[n] = ++y);
}
return 0;
}
本文来自博客园,作者:caijianhong,转载请注明原文链接:https://www.cnblogs.com/caijianhong/p/19011019
浙公网安备 33010602011771号