【数据结构】可持久化线段树

一、为什么需要可持久化?

1.1 普通线段树的局限

普通线段树在进行 update 操作时,会直接修改原节点。这意味着:

  • 旧版本的数据被覆盖,无法访问。
  • 如果你想知道“第3次修改前,区间[1,5]的和是多少?”,普通线段树做不到。

1.2 可持久化的目标

可持久化 = 能够访问任意历史版本的数据结构

就像 Git 版本控制系统一样,每次修改都生成一个新版本,但旧版本依然可用。


二、核心思想:路径复制 + 节点共享

这是可持久化线段树的灵魂

2.1 问题:如果复制整棵树,空间爆炸!

  • 每次更新复制整棵树 → 空间 O(n2)O(n^2)O(n2),不可接受。

2.2 关键洞察:只有路径上的节点变了!

当我们更新一个叶子节点时,只有从根到该叶子的路径上的节点的统计信息(如 cnt)会改变。

其余子树完全不变,可以共享

2.3 解决方案:只复制路径上的节点

  • 复制从根到目标叶子路径上的所有节点。
  • 其他子树用指针指向原树的对应节点。
  • 每次更新生成一个新根,代表新版本。

结果:每次更新只新增 O(log⁡n)O(\log n)O(logn) 个节点,空间可控!


三、经典应用:静态区间第K小

我们以这个最经典的问题为例,彻底拆解每一步。

问题:

给定数组 a[1..n],多次询问 query(l, r, k):求区间 [l, r] 中第 k 小的数。

思路:

  1. 离散化:将数值映射到排名(1~n),便于建树。
  2. 前缀建树:对每个前缀 [1..i] 建一棵线段树,记录每个值出现了多少次。
  3. 差分查询:用 T[r] - T[l-1] 得到 [l,r] 的频次分布,然后二分找第k小。

四、详细步骤拆解

步骤1:离散化(Coordinate Compression)

原始数组:[100, 200, 100, 300, 200]

离散化过程:

  1. 提取所有值:[100,200,100,300,200]
  2. 排序去重:[100,200,300]
  3. 映射: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]:当前线段树区间(离散化后的值域)
  1. 计算左子树中 [l,r] 区间有多少个数:
    cnt_left = tree[左子树_r].cnt - tree[左子树_l].cnt
    
  2. 如果 k <= cnt_left,说明第k小在左半区间,递归左子树。
  3. 否则,在右半区间,递归右子树,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(nlog⁡n)O(n \log n)O(nlogn)O(nlog⁡n)O(n \log n)O(nlogn)
每次查询O(log⁡n)O(\log n)O(logn)-
每次更新O(log⁡n)O(\log n)O(logn)O(log⁡n)O(\log n)O(logn) 新增节点

空间估算:最多 nlog⁡nn \log nnlogn 个节点,n=105n=10^5n=105 时约 1.7×1061.7 \times 10^61.7×106 节点,可接受。


八、常见误区与调试技巧

❌ 误区1:认为每次都要复制整棵树

  • ✅ 正确:只复制路径上的 O(log⁡n)O(\log n)O(logn) 个节点。

❌ 误区2:query 时只传一个根

  • ✅ 正确:必须传两个根(l-1r)做差分。

❌ 误区3:忘记离散化

  • 大值域下直接建树会 MLE。

🔧 调试技巧:

  1. 打印每个版本的根和总 cnt
  2. 对小数据手动模拟建树过程。
  3. updatequery 中打印递归路径。

九、扩展思考

9.1 动态区间第K大?

  • 需要树状数组套主席树(带修主席树),更复杂。

9.2 可持久化其他结构?

  • 可持久化平衡树(如 Treap)
  • 可持久化并查集(按秩合并 + 不路径压缩)

9.3 函数式编程思想

  • 每次操作返回新结构,原结构不变 → 纯函数式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值