替罪羊树
以下是我按照下方代码重新写了一遍之后的代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
const double alpha = 0.7; // 平衡因子
int root, idx, val[maxn], cnt[maxn], sz[maxn], ls[maxn], rs[maxn];
// 创建新节点,返回编号
int new_node(int x) {
val[++idx] = x;
cnt[idx] = sz[idx] = 1;
ls[idx] = rs[idx] = 0;
return idx;
}
// 更新子树大小
void push_up(int u) {
sz[u] = sz[ ls[u] ] + sz[ rs[u] ] + cnt[u];
}
// 重构相关
vector<int> nodes; // 存放中序遍历的节点
void dfs(int u) {
if (!u) return;
dfs(ls[u]);
if (cnt[u] > 0) nodes.push_back(u);
dfs(rs[u]);
}
int build(int l, int r) {
if (l > r) return 0;
int mid = (l + r) >> 1;
int u = nodes[mid];
ls[u] = build(l, mid-1);
rs[u] = build(mid+1, r);
push_up(u);
return u;
}
// 拍扁并重建子树 u
void rebuild(int &u) {
nodes.clear();
dfs(u);
u = build(0, nodes.size() - 1);
}
// 插入: 返回值表示该子树是否已经执行过重构
bool Insert(int &u, int x) {
if (!u) {
u = new_node(x);
return false;
}
bool done = false;
if (x == val[u]) {
cnt[u]++;
sz[u]++;
return false;
}
else {
done = (x < val[u]) ? Insert(ls[u], x) : Insert(rs[u], x);
}
push_up(u);
if (done) // 孩子已经重构,本层无需再检查
return true;
if (max(sz[ ls[u] ], sz[ rs[u] ]) > sz[u] * alpha) {
rebuild(u);
return true;
}
return false;
}
// 删除(懒惰删除)
void Delete(int &u, int x) {
if (val[u] == x) {
if (cnt[u] > 0) {
cnt[u]--;
sz[u]--;
}
return;
}
(x < val[u]) ? Delete(ls[u], x) : Delete(rs[u], x);
push_up(u);
}
// 查询排名:比 x 小的数有多少个
int Rank(int u, int x) {
if (!u) return 0;
if (val[u] < x)
return sz[ ls[u] ] + cnt[u] + Rank(rs[u], x);
else
return Rank(ls[u], x);
}
// 查询第 k 小的数
int Kth(int u, int k) {
if (!u) return -1; // 不会发生,因为题目保证存在
if (sz[ ls[u] ] >= k)
return Kth(ls[u], k);
else if (sz[ ls[u] ] + cnt[u] >= k)
return val[u];
else
return Kth(rs[u], k - sz[ ls[u] ] - cnt[u]);
}
// 前驱
int Pre(int x) {
return Kth(root, Rank(root, x));
}
// 后继
int Suc(int x) {
return Kth(root, Rank(root, x+1) + 1);
}
int n;
int main() {
scanf("%d", &n);
for (int i = 0, op, x; i < n; i++) {
scanf("%d%d", &op, &x);
if (op == 1) {
Insert(root, x);
}
else if (op == 2) {
Delete(root, x);
}
else if (op == 3) {
printf("%d\n", Rank(root, x) + 1);
}
else if (op == 4) {
printf("%d\n", Kth(root, x));
}
else if (op == 5) {
printf("%d\n", Pre(x));
}
else {
printf("%d\n", Suc(x));
}
}
return 0;
}
然后你们可能和我一样有一个疑问,就是:
为什么子树中重构了,当前节点对应的子树就不用重构了?
这是因为:
在替罪羊树中,之所以“子树中发生了重构,自己就不用再重构”,核心原因在于一次插入至多只会让从叶到根路径上的一个节点违反重量平衡条件(只要选取的平衡因子 (\alpha) 合适,如常用的 (0.7) 左右)。
重构正是针对这唯一的“替罪羊”节点进行的,一旦它在较低层被找到并重构,它的所有祖先就一定仍然满足平衡条件,自然不需要再次重构。
具体逻辑如下:
-
平衡条件
替罪羊树是重量平衡树,每个节点 (u) 都必须满足:\[\max( \text{size}(u.\text{left}), \text{size}(u.\text{right}) ) \le \alpha \cdot \text{size}(u) \]其中 \(\alpha\) 是预设常数(例如 \(0.66\) 或 \(0.75\))。
-
插入引发的变化
插入一个新结点后,从该叶到根的路径上每个结点的大小都会 \(+1\)。原本紧贴 \(\alpha\) 边界的结点就可能越过阈值,成为不平衡结点。 -
为什么路径上只有一个不平衡结点
可以证明(且实践中通常选取 \(\alpha \ge 0.66\)):插入仅会导致路径上最深(最靠近叶子)的那一个结点违反平衡条件,而其所有祖先仍然满足 \(\max(\text{left},\text{right}) \le \alpha \cdot \text{size}\)。
也就是说,不平衡结点最多只有一个。 -
回溯与重构策略
插入后的回溯会从新结点向上检查每个祖先是否平衡:- 如果在某个后代(子树)中发现了不平衡并执行了重构,那它一定就是那个唯一的“替罪羊”。
- 重构会把这棵子树改造成完全平衡的结构,子树的大小不变,但内部高度降到最低。
- 既然唯一的“罪魁祸首”已经被修复,它之上的祖先结点在插入后本来就没违反条件(因为只有这一个结点失衡),现在子树的重量也没变,自然依然平衡,不需要再次重构。
-
代码层面的体现
在递归实现中,通常用一个标志位向上传递“是否已经发生过重构”。一旦某层发现子树已重构,便跳过自己的平衡检查,直接向上返回。这保证了一次插入最多只做一次重建,且重建的一定是最深的不平衡结点。
一句话总结:
在恰当的 \(\alpha\) 下,插入造成的重量失衡只会出现在最深的那个祖先上;把它重构掉之后,上面的所有祖先都自动维持平衡,因此完全不必再次重构。
作者声明:以下内容由AI生成。主要用于我自己学习、
以下是使用“替罪羊树”解决 P3369 【模板】普通平衡树 的 C++ 代码,以及每个函数的功能解释。
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
const double ALPHA = 0.7; // 平衡因子
int root, idx; // root 根节点编号,idx 节点分配器
int val[N], cnt[N], sz[N]; // 值,出现次数,子树大小(含cnt)
int ls[N], rs[N]; // 左右孩子
// 创建新节点,返回编号
int newnode(int x) {
++idx;
val[idx] = x;
cnt[idx] = sz[idx] = 1;
ls[idx] = rs[idx] = 0;
return idx;
}
// 更新子树大小
void pushup(int u) {
sz[u] = sz[ls[u]] + sz[rs[u]] + cnt[u];
}
// ============ 重构相关 ============
vector<int> nodes; // 存放中序遍历的节点
// 中序遍历,将有效的节点(cnt > 0)存入 nodes
void dfs(int u) {
if (!u) return;
dfs(ls[u]);
if (cnt[u] > 0) nodes.push_back(u);
dfs(rs[u]);
}
// 从 nodes[l..r] 建树,返回树根
int build(int l, int r) {
if (l > r) return 0;
int mid = (l + r) >> 1;
int u = nodes[mid];
ls[u] = build(l, mid - 1);
rs[u] = build(mid + 1, r);
pushup(u);
return u;
}
// 拍扁并重建子树 u
void rebuild(int &u) {
nodes.clear();
dfs(u);
u = build(0, nodes.size() - 1);
}
// ============ 插入 ============
// 返回值表示该子树是否已经执行过重构
bool Insert(int &u, int x) {
if (!u) {
u = newnode(x);
return false;
}
bool done = false;
if (x == val[u]) {
++cnt[u]; ++sz[u];
return false;
} else if (x < val[u]) {
done = Insert(ls[u], x);
} else {
done = Insert(rs[u], x);
}
pushup(u);
if (done) return true; // 孩子已经重构,本层无需再检查
if (sz[ls[u]] > ALPHA * sz[u] || sz[rs[u]] > ALPHA * sz[u]) {
rebuild(u);
return true;
}
return false;
}
// ============ 删除(懒惰删除) ============
void Delete(int &u, int x) {
if (val[u] == x) {
if (cnt[u] > 0) {
--cnt[u]; --sz[u];
}
return;
} else if (x < val[u]) {
Delete(ls[u], x);
} else {
Delete(rs[u], x);
}
pushup(u);
}
// ============ 查询排名:比 x 小的数有多少个 ============
int Rank(int u, int x) {
if (!u) return 0;
if (val[u] < x) {
return sz[ls[u]] + cnt[u] + Rank(rs[u], x);
} else {
return Rank(ls[u], x);
}
}
// ============ 查询第 k 小的数 ============
int Kth(int u, int k) {
if (!u) return -1; // 不会发生,因为题目保证存在
if (sz[ls[u]] >= k) return Kth(ls[u], k);
else if (sz[ls[u]] + cnt[u] >= k) return val[u];
else return Kth(rs[u], k - sz[ls[u]] - cnt[u]);
}
// 前驱
int Precursor(int x) {
return Kth(root, Rank(root, x));
}
// 后继
int Successor(int x) {
return Kth(root, Rank(root, x + 1) + 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n; cin >> n;
while (n--) {
int opt, x; cin >> opt >> x;
if (opt == 1) {
Insert(root, x);
} else if (opt == 2) {
Delete(root, x);
} else if (opt == 3) {
cout << Rank(root, x) + 1 << '\n';
} else if (opt == 4) {
cout << Kth(root, x) << '\n';
} else if (opt == 5) {
cout << Precursor(x) << '\n';
} else { // opt == 6
cout << Successor(x) << '\n';
}
}
return 0;
}
函数功能说明
| 函数 | 功能 |
|---|---|
newnode(x) |
分配一个新节点,值为 x,初始计数和子树大小均为 1,左右孩子为空,返回节点编号。 |
pushup(u) |
更新节点 u 的子树大小 sz[u] = 左子树大小 + 右子树大小 + 当前节点的 cnt[u]。 |
dfs(u) |
中序遍历以 u 为根的子树,将 有效节点(cnt > 0)的编号存入全局数组 nodes,用于重构。 |
build(l, r) |
将 nodes[l..r] 构建成高度平衡的二叉搜索树,返回根节点编号。 |
rebuild(u) |
对子树 u 进行重构:先 dfs 收集有效节点,再通过 build 建树,最后将新根赋给 u。 |
Insert(u, x) |
向子树 u 中插入值 x。若存在则增加 cnt;否则递归插入空位。回溯时自底向上检查是否需要重构(任一子树大小超过 ALPHA * 当前子树大小 即触发重构)。返回值为 bool,表示该子树内部是否已执行重构,以避免祖先重复检查。 |
Delete(u, x) |
从子树 u 中删除一个 x(懒惰删除)。找到对应节点后直接将 cnt 和 sz 减 1,不立即移除节点。删除后仅更新大小,不重构(在后续插入中会被自然清理)。 |
Rank(u, x) |
返回子树 u 中 小于 x 的元素总个数。利用 BST 性质递归统计,注意若节点值小于 x 则加上左子树大小和节点计数,然后向右走。 |
Kth(u, k) |
返回子树 u 中 第 k 小的元素。若左子树大小 ≥ k 则向左;否则若加上当前节点计数 ≥ k 则返回当前节点值;否则向右递归并调整 k 值。 |
Precursor(x) |
查询 x 的前驱(小于 x 的最大数)。等于集合中第 Rank(x) 小的数。 |
Successor(x) |
查询 x 的后继(大于 x 的最小数)。等于集合中第 Rank(x+1) + 1 小的数。 |
设计思路:利用替罪羊树的懒惰删除 + 插入时按 ALPHA 阈值重构,既保持了均摊 \(O(\log n)\) 的复杂度,又避免了真删除带来的复杂指针操作;同时利用 Rank 和 Kth 组合优雅地完成了前驱/后继查询。
浙公网安备 33010602011771号