可持久化线段树(主席树)
💡 核心思想
可持久化线段树(又称主席树)是在每次修改时保留历史版本的线段树。通过"路径复制"技巧:每次修改只创建 O(log n) 个新节点,其余节点共享原树的节点。这样可以在 O(log n) 时间内查询任意历史版本。经典应用:区间第 K 大(静态)、区间修改的历史版本查询。
主席树是 NOI 级别的数据结构,CSP-S 偶尔出现。它的核心思想是"只修改必要的节点"。普通线段树修改需要 O(log n) 时间,主席树也只需要 O(log n) 空间(新增节点),因为未修改的子树直接共享原树的指针。理解了这个,代码就很简单:修改时递归,左/右子树有一个被修改就创建新节点,另一个直接指向原节点。
🎯 直觉理解
想象你在写一份重要文档,每次修改前都先"另存为"一个新版本。普通做法是复制整份文档(O(n)),而主席树的做法是:只复制你修改的那几页(O(log n)),其他页直接引用旧版本。这样你就有了一份完整的历史版本库,可以随时回到任意版本查看。
📝 算法流程
- 建立权值线段树(按值域而非下标)
- 每次修改(单点更新):
- 创建新根节点
- 递归修改:被修改的子树创建新节点,另一个子树直接指向原节点
- 查询区间 [l,r] 的第 K 大:
- 用 root[r] 和 root[l-1] 两个版本同时查询
- 左子树的节点数差 = 区间中落在左子树的数的个数
- 如果 >= K,递归左子树;否则递归右子树,K 减去左子树个数
$$\text{节点数:}O(n \log n)\text{(每次修改新增 }\log n\text{ 个节点)}$$
📊 复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间 | $O(n \log n)$ 建树 / $O(\log n)$ 单次查询 |
| 空间 | $O(n \log n)$ |
💻 参考实现(C++)
C++ (C++17)
#include
using namespace std;
const int N = 200005;
struct Node { int l, r, sum; } tr[N * 20];
int root[N], cnt;
int a[N], b[N]; // b 为离散化后的值
int build(int l, int r) {
int p = ++cnt;
if (l == r) return p;
int mid = (l + r) >> 1;
tr[p].l = build(l, mid);
tr[p].r = build(mid + 1, r);
return p;
}
int update(int p, int l, int r, int pos) {
int q = ++cnt;
tr[q] = tr[p]; tr[q].sum++;
if (l == r) return q;
int mid = (l + r) >> 1;
if (pos <= mid) tr[q].l = update(tr[p].l, l, mid, pos);
else tr[q].r = update(tr[p].r, mid + 1, r, pos);
return q;
}
int query(int p, int q, int l, int r, int k) {
if (l == r) return l;
int mid = (l + r) >> 1;
int leftSum = tr[tr[q].l].sum - tr[tr[p].l].sum;
if (leftSum >= k) return query(tr[p].l, tr[q].l, l, mid, k);
else return query(tr[p].r, tr[q].r, mid + 1, r, k - leftSum);
}
int main() {
int n = 5, m = 5;
for (int i = 1; i <= n; i++) cin >> a[i];
// 离散化
memcpy(b, a, sizeof(a));
sort(b + 1, b + n + 1);
int sz = unique(b + 1, b + n + 1) - (b + 1);
root[0] = build(1, sz);
for (int i = 1; i <= n; i++) {
int pos = lower_bound(b + 1, b + sz + 1, a[i]) - b;
root[i] = update(root[i-1], 1, sz, pos);
}
// 查询 [l,r] 第 k 大
cout << b[query(root[0], root[n], 1, sz, 3)] << endl;
return 0;
} ⚠️ 常见坑点
节点数组开太小——主席树需要约 n·log(n) 个节点
忘记离散化——权值线段树需要离散化值域
查询时下标混淆——root[r] 和 root[l-1] 不要搞反
sum 的更新——新节点的 sum = 原节点.sum + 1
📚 相关题目
| 题目 | 来源 | 难度 | 备注 |
|---|---|---|---|
| P3834 可持久化线段树 | 洛谷 | 提高组 | 主席树模板题 |
| P2617 Dynamic Rankings | 洛谷 | 提高组 | 主席树 + 树状数组 |
| P3302 题目难度 | 洛谷 | 提高组 | 主席树 + LCA |