一、为什么需要可持久化?
1.1 普通线段树的局限
普通线段树在进行 update 操作时,会直接修改原节点。这意味着:
- 旧版本的数据被覆盖,无法访问。
- 如果你想知道“第3次修改前,区间[1,5]的和是多少?”,普通线段树做不到。
1.2 可持久化的目标
可持久化 = 能够访问任意历史版本的数据结构
就像 Git 版本控制系统一样,每次修改都生成一个新版本,但旧版本依然可用。
二、核心思想:路径复制 + 节点共享
这是可持久化线段树的灵魂。
2.1 问题:如果复制整棵树,空间爆炸!
- 每次更新复制整棵树 → 空间 O(n2)O(n^2)O(n2),不可接受。
2.2 关键洞察:只有路径上的节点变了!
当我们更新一个叶子节点时,只有从根到该叶子的路径上的节点的统计信息(如 cnt)会改变。
其余子树完全不变,可以共享。
2.3 解决方案:只复制路径上的节点
- 复制从根到目标叶子路径上的所有节点。
- 其他子树用指针指向原树的对应节点。
- 每次更新生成一个新根,代表新版本。
✅ 结果:每次更新只新增 O(logn)O(\log n)O(logn) 个节点,空间可控!
三、经典应用:静态区间第K小
我们以这个最经典的问题为例,彻底拆解每一步。
问题:
给定数组 a[1..n],多次询问 query(l, r, k):求区间 [l, r] 中第 k 小的数。
思路:
- 离散化:将数值映射到排名(1~n),便于建树。
- 前缀建树:对每个前缀
[1..i]建一棵线段树,记录每个值出现了多少次。 - 差分查询:用
T[r] - T[l-1]得到[l,r]的频次分布,然后二分找第k小。
四、详细步骤拆解
步骤1:离散化(Coordinate Compression)
原始数组:[100, 200, 100, 300, 200]
离散化过程:
- 提取所有值:
[100,200,100,300,200] - 排序去重:
[100,200,300] - 映射:
100→1, 200→2, 300→3
好处:值域从可能的 10910^9109 缩小到 nnn,线段树只需 [1,n][1,n][1,n]。
步骤2:构建版本树(前缀插入)
我们按顺序插入每个元素,每次生成一个新版本。
初始:空树(版本0)
版本0: 所有 cnt = 0
插入 a[1]=100 → 映射为1
- 从根开始,沿路径
1向下。 - 复制路径上所有节点(本例中只复制根和叶子)。
- 更新路径上节点的
cnt += 1。 - 新根为版本1。
插入 a[2]=200 → 映射为2
- 从版本1的根开始。
- 沿路径
2向下复制。 - 生成版本2。
关键:未修改的子树(如值1的分支)直接共享版本1的节点。
以此类推…
最终我们有 n+1 个版本(0~n),roots[i] 表示前 i 个元素的统计。
步骤3:查询 [l,r] 第k小
如何得到 [l,r] 的频次?
- 版本
r的树:包含[1..r]的频次 - 版本
l-1的树:包含[1..l-1]的频次 - 相减 →
[l..r]的频次
查询过程(递归二分)
int query(root_l, root_r, l_range, r_range, k)
root_l:版本l-1的根root_r:版本r的根[l_range, r_range]:当前线段树区间(离散化后的值域)
- 计算左子树中
[l,r]区间有多少个数:cnt_left = tree[左子树_r].cnt - tree[左子树_l].cnt - 如果
k <= cnt_left,说明第k小在左半区间,递归左子树。 - 否则,在右半区间,递归右子树,
k = k - cnt_left。
五、C++ 实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 100005; // 最大数组长度
// ========== 线段树节点定义 ==========
struct Node {
int left_child; // 左子节点在 tree 数组中的索引
int right_child; // 右子节点在 tree 数组中的索引
int count; // 当前区间内数字出现的总次数
// 构造函数
Node() : left_child(0), right_child(0), count(0) {}
};
// ========== 全局数据结构 ==========
vector<Node> tree; // 节点池,动态分配
vector<int> root_versions; // root_versions[i] = 第i个版本的根节点索引
vector<int> original_arr; // 原始数组 a[1..n]
vector<int> sorted_unique; // 离散化后的有序唯一值数组
// ========== 离散化函数 ==========
// 输入原值 x,返回其在 sorted_unique 中的排名(从1开始)
int get_rank(int x) {
// lower_bound 找到第一个 >= x 的位置
auto pos = lower_bound(sorted_unique.begin(), sorted_unique.end(), x);
return pos - sorted_unique.begin() + 1; // +1 因为从1开始编号
}
// ========== 构建空树(初始化) ==========
// 递归构建一棵空的线段树,覆盖值域 [1, size]
// 返回该树根节点在 tree 中的索引
int build_empty_tree(int start, int end) {
int node_id = tree.size(); // 当前节点在 tree 中的索引
tree.push_back(Node()); // 添加新节点
// 叶子节点:区间 [x,x]
if (start == end) {
return node_id; // 无需设置左右孩子(默认0)
}
int mid = (start + end) / 2;
// 递归构建左右子树
tree[node_id].left_child = build_empty_tree(start, mid);
tree[node_id].right_child = build_empty_tree(mid + 1, end);
// 非叶子节点的 count 会在 update 时更新,这里保持0
return node_id;
}
// ========== 更新操作(插入一个数) ==========
// 参数:
// old_root: 旧版本的根节点索引
// range_l, range_r: 当前节点覆盖的值域区间
// pos: 要插入的数值的排名(离散化后的)
// value: +1(插入)或 -1(删除)
// 返回:新版本根节点的索引
int update_version(int old_root, int range_l, int range_r, int pos, int value) {
int new_node_id = tree.size();
tree.push_back(tree[old_root]); // 复制旧节点的所有信息(包括左右孩子和count)
// 叶子节点:直接修改计数
if (range_l == range_r) {
tree[new_node_id].count += value;
return new_node_id;
}
int mid = (range_l + range_r) / 2;
// 决定往哪边走,并递归更新
if (pos <= mid) {
// 走左子树
int new_left_child = update_version(
tree[old_root].left_child, // 旧左子树根
range_l, mid, // 新区间
pos, value // 相同参数
);
tree[new_node_id].left_child = new_left_child;
} else {
// 走右子树
int new_right_child = update_version(
tree[old_root].right_child,
mid + 1, range_r,
pos, value
);
tree[new_node_id].right_child = new_right_child;
}
// 更新当前节点的总数量
tree[new_node_id].count =
tree[tree[new_node_id].left_child].count +
tree[tree[new_node_id].right_child].count;
return new_node_id;
}
// ========== 查询操作:找第k小 ==========
// 返回第k小的数在离散化数组中的排名
// root_left_version: 版本 l-1 的根
// root_right_version: 版本 r 的根
// val_l, val_r: 当前值域区间
// k: 要找第几小
int query_kth(int root_left_version, int root_right_version,
int val_l, int val_r, int k) {
// 叶子节点:找到了!
if (val_l == val_r) {
return val_l;
}
int mid = (val_l + val_r) / 2;
// 计算在左半区间 [val_l, mid] 内,[l,r] 区间中有多少个数
// = 版本r的左子树数量 - 版本l-1的左子树数量
int left_count_in_r = tree[tree[root_right_version].left_child].count;
int left_count_in_l_minus = tree[tree[root_left_version].left_child].count;
int total_left_count = left_count_in_r - left_count_in_l_minus;
if (k <= total_left_count) {
// 第k小在左半区间
return query_kth(
tree[root_left_version].left_child, // 左版本的左子树
tree[root_right_version].left_child, // 右版本的左子树
val_l, mid, // 新值域
k // k不变
);
} else {
// 第k小在右半区间
return query_kth(
tree[root_left_version].right_child,
tree[root_right_version].right_child,
mid + 1, val_r,
k - total_left_count // 减去左边的数量
);
}
}
// ========== 主函数 ==========
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
// 初始化数组
original_arr.resize(n + 1); // 1-indexed
sorted_unique.resize(n); // 临时存储原值
// 读入数据
for (int i = 1; i <= n; ++i) {
cin >> original_arr[i];
sorted_unique[i - 1] = original_arr[i];
}
// 离散化:排序 + 去重
sort(sorted_unique.begin(), sorted_unique.end());
auto last = unique(sorted_unique.begin(), sorted_unique.end());
sorted_unique.erase(last, sorted_unique.end());
int value_domain_size = sorted_unique.size(); // 离散化后的值域大小
// ========== 初始化可持久化线段树 ==========
tree.clear();
root_versions.resize(n + 1); // 版本 0 ~ n
// 版本0:空树
root_versions[0] = build_empty_tree(1, value_domain_size);
// 构建版本 1 ~ n
for (int i = 1; i <= n; ++i) {
int rank = get_rank(original_arr[i]); // 得到排名
// 基于前一个版本插入新数
root_versions[i] = update_version(
root_versions[i - 1], // 旧版本根
1, value_domain_size, // 值域
rank, 1 // 插入排名为rank的数,+1
);
}
// ========== 处理 m 次查询 ==========
for (int i = 0; i < m; ++i) {
int l, r, k;
cin >> l >> r >> k;
// 查询 [l,r] 区间第k小
int rank = query_kth(
root_versions[l - 1], // 版本 l-1
root_versions[r], // 版本 r
1, value_domain_size, // 值域
k // 第k小
);
// 将排名转回原值输出
cout << sorted_unique[rank - 1] << '\n';
}
return 0;
}
六、可视化理解(以 [1,3,2,3,1] 为例)
离散化:
- 原值:[1,3,2,3,1]
- 排序去重:[1,2,3]
- 映射:1→1, 2→2, 3→3
版本构建过程:
版本0: 空树
[0]
/ \
[0] [0]
/ \ / \
1 2 3 4
版本1: 插入1
[1] ← 新节点
/ \
[1] [0] ← 左子树复制,右子树共享版本0
/ \ / \
1 2 3 4
版本2: 插入3
[1]
/ \
[1] [1] ← 右子树路径复制
/ \ / \
1 2 3 4 ← 叶子3的计数+1
注意:未修改的节点(如左子树)直接复用旧版本。
七、复杂度分析
| 操作 | 时间 | 空间 |
|---|---|---|
| 构建n个版本 | O(nlogn)O(n \log n)O(nlogn) | O(nlogn)O(n \log n)O(nlogn) |
| 每次查询 | O(logn)O(\log n)O(logn) | - |
| 每次更新 | O(logn)O(\log n)O(logn) | O(logn)O(\log n)O(logn) 新增节点 |
空间估算:最多 nlognn \log nnlogn 个节点,n=105n=10^5n=105 时约 1.7×1061.7 \times 10^61.7×106 节点,可接受。
八、常见误区与调试技巧
❌ 误区1:认为每次都要复制整棵树
- ✅ 正确:只复制路径上的 O(logn)O(\log n)O(logn) 个节点。
❌ 误区2:query 时只传一个根
- ✅ 正确:必须传两个根(
l-1和r)做差分。
❌ 误区3:忘记离散化
- 大值域下直接建树会 MLE。
🔧 调试技巧:
- 打印每个版本的根和总
cnt。 - 对小数据手动模拟建树过程。
- 在
update和query中打印递归路径。
九、扩展思考
9.1 动态区间第K大?
- 需要树状数组套主席树(带修主席树),更复杂。
9.2 可持久化其他结构?
- 可持久化平衡树(如 Treap)
- 可持久化并查集(按秩合并 + 不路径压缩)
9.3 函数式编程思想
- 每次操作返回新结构,原结构不变 → 纯函数式。
4559

被折叠的 条评论
为什么被折叠?



