当前位置: 首页 > news >正文

平衡树的一些记录和带插入区间K小值

平衡树和带插入区间K小值

高速平衡树

除了 Splay 和无旋 Treap 以外的平衡树都是高速平衡树。忘了出处是哪里了,好像是 shadowice1984。

无旋 Treap 的一些写法

随机权值

Treap 的随机权值可以不维护。假如权值是小根堆,那么点 \(p\) 的随机权值可以认为是 \(siz_p\) 个随机数中的最小值。那么我们在比较两个点的随机权值时,使它有 \(\dfrac{siz_p}{siz_p+siz_q}\) 的概率是 \(p\) 的权值小于 \(q\) 的权值,反之亦然。

插入删除只用一次 split/merge

Treap - skip2004 - 博客园

假设随机值是小根堆,对于插入,我们比较随机值,如果随机值比树根,就把这个树按照插入权值 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 LOCALcin.tie(nullptr)->sync_with_stdio(false);
#endifcin >> 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;
}
http://www.sczhlp.com/news/1042/

相关文章:

  • 基于块匹配的全景图像拼接
  • 【ACM独立出版、EI快速稳定检索】第二届虚拟现实、图像和信号处理国际学术会议(VRISP 2025)
  • BMP图像原理与应用
  • 亚马逊AI模型评估产品评论中的实用建议有效性
  • DNS协议
  • Python数据结构(列表、字典、元祖)
  • C#调用邮箱应用发送带附件的邮件
  • Air780EGH定位开发速成指南:源代码公开,即学即用
  • Splunk Enterprise 10.0.0 发布,新增功能简介
  • Studio 3T 2025.13 (macOS, Linux, Windows) - MongoDB 的终极 GUI、IDE 和 客户端
  • 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-24- 操作Select下拉选择框 - 上篇(详细教程) - 北京
  • delphi7 中文企业版编译minipad2
  • 【PCIE725-1 】基于 PCIe x16 总线架构的 JFM9VU9P FPGA 高性能数据预处理平台(100%国产化)
  • Prometheus源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:Discovery 动态服务发现机制
  • 在运维工作中,Docker的运行状态有哪些?
  • BZOJ 4641 题解
  • APP UI自动化元素定位高频问题
  • 通义灵码保姆级教程:从数据读取、清洗、结合大模型分析、可视化、生成报告全链路
  • 在运维工作中,docker file 用什么构建容器的?
  • 一维光栅结构严格耦合波分析(RCWA)求解器
  • rust学习笔记之基础:类型系统和类型转换
  • 在运维工作中,Docker的基本命令有哪些?
  • 云原生周刊:2025年的服务网格
  • 故障处理:troubleshooting Cluster Time Synchronization Service
  • 在运维工作中,镜像启动一个容器的命令的什么?
  • python命令行解析模块argparse
  • 学习笔记:一次RMAN还原慢的分析
  • 抖音Next-User Retrieva:生成式冷启动召回
  • 求两个自然数a和b的最大公约数(递归算法)
  • nginx压缩字体ttf的有关配置