Persistent Segment Trees: Implementation and Applications

Core Concept

Persistent Segmant Trees, also known as Persistant (Value Domain) Segment Trees, maintain multiple versions of segment trees while supporting historical queries.

Time complexity: O(log n)
Total space complexity: O(m log n + 4n)

These trees preserve all historical versions while supporting standard segment tree operations and queries across any previous version.

Key Implementation Strategy

Dynamic node allocation avoids the O(4nm) complexity of brute-force tree copying. Since only the path from root to modified leaf nodes differs between consecutive versions, we only duplicate this path, achieving O(log n) complexity per update.

Practical Applications

Static Range K-th Smallest Element

Use persistent trees to maintain prefix sums [1, i] for value domain counts. Tree structures remain consistent (with shared nodes), allowing subtraction between trees at positions k_r and k_{l-1} to isolate [l, r] contributions. Perform binary search on the resulting segment tree after discretization.

Dynamic Inverse Pairs with Updates

Combine persistent trees with Fenwick tree approach. Modify trees at positions p, p + lowbit(p), etc., maintaining prefix sums via Fenwick structure. Calculate initial inverse pairs, then subtract contributions from deleted numbers while accounting for remaining valid pairs using persistent tree queries.

Time complexity: O(n log² n)

K-th Element on Tree Paths

Maintain persistent trees for root-to-node paths. For nodes u and v, path contribution equals:

g(u) + g(v) - g(lca(u, v)) - g(parent[lca(u, v)])

Query k-th largest element directly on the persistent tree.

Range Updates with Historical Queries

Handle interval modifications using lazy propagation with persistence. Apply permanent markers - update values and lazy flags only at completely covered nodes. During queries, accumulate lazy sums from root to current parenet nodes.

int modify_tree(int prev, int left, int right, int qleft, int qright, int delta) {
    int current = ++node_count;
    left_child[current] = left_child[prev];
    right_child[current] = right_child[prev];
    tree_sum[current] = tree_sum[prev];
    lazy[current] = lazy[prev];
    
    if (left >= qleft && right <= qright) {
        lazy[current] += delta;
        tree_sum[current] += (long long)delta * (right - left + 1);
        return current;
    }
    
    int mid = (left + right) >> 1;
    if (qleft <= mid) 
        left_child[current] = modify_tree(left_child[prev], left, mid, qleft, qright, delta);
    if (qright > mid) 
        right_child[current] = modify_tree(right_child[prev], mid + 1, right, qleft, qright, delta);
    
    tree_sum[current] = tree_sum[left_child[current]] + tree_sum[right_child[current]] + 
                       (right - left + 1) * lazy[current];
    return current;
}

long long range_query(int node, int left, int right, int qleft, int qright, long long accumulated_lazy) {
    if (left >= qleft && right <= qright) 
        return tree_sum[node] + (right - left + 1) * accumulated_lazy;
    
    int mid = (left + right) >> 1;
    long long result = 0;
    if (qleft <= mid)
        result += range_query(left_child[node], left, mid, qleft, qright, accumulated_lazy + lazy[node]);
    if (qright > mid)
        result += range_query(right_child[node], mid + 1, right, qleft, qright, accumulated_lazy + lazy[node]);
    return result;
}

Implementation Example: K-th Smallest Query

#include <cstdio>
#include <algorithm>
using namespace std;

const int MAX_N = 200005;

int node_count;
int original[MAX_N], sorted[MAX_N], mapping[MAX_N], roots[MAX_N];
int left_sub[MAX_N * 32], right_sub[MAX_N * 32], count[MAX_N * 32];

int construct_tree(int start, int end) {
    int current = ++node_count;
    if (start < end) {
        int mid = (start + end) >> 1;
        left_sub[current] = construct_tree(start, mid);
        right_sub[current] = construct_tree(mid + 1, end);
    }
    return current;
}

int insert_node(int previous, int start, int end, int position) {
    int current = ++node_count;
    left_sub[current] = left_sub[previous];
    right_sub[current] = right_sub[previous];
    count[current] = count[previous] + 1;
    
    if (start < end) {
        int mid = (start + end) >> 1;
        if (position <= mid)
            left_sub[current] = insert_node(left_sub[previous], start, mid, position);
        else
            right_sub[current] = insert_node(right_sub[previous], mid + 1, end, position);
    }
    return current;
}

int find_kth(int left_node, int right_node, int start, int end, int k) {
    if (start == end) return start;
    
    int left_count = count[left_sub[left_node]] - count[left_sub[right_node]];
    int mid = (start + end) >> 1;
    
    if (k <= left_count)
        return find_kth(left_sub[left_node], left_sub[right_node], start, mid, k);
    else
        return find_kth(right_sub[left_node], right_sub[right_node], mid + 1, end, k - left_count);
}

int main() {
    int n, queries;
    scanf("%d%d", &n, &queries);
    
    for (int i = 1; i <= n; i++) {
        scanf("%d", &original[i]);
        sorted[i] = original[i];
    }
    
    sort(sorted + 1, sorted + n + 1);
    int unique_count = unique(sorted + 1, sorted + n + 1) - sorted - 1;
    
    roots[0] = construct_tree(1, unique_count);
    
    for (int i = 1; i <= n; i++) {
        mapping[i] = lower_bound(sorted + 1, sorted + unique_count + 1, original[i]) - sorted;
        roots[i] = insert_node(roots[i - 1], 1, unique_count, mapping[i]);
    }
    
    while (queries--) {
        int l, r, k;
        scanf("%d%d%d", &l, &r, &k);
        int pos = find_kth(roots[r], roots[l - 1], 1, unique_count, k);
        printf("%d\n", sorted[pos]);
    }
    
    return 0;
}

Tags: persistent-segment-tree range-query discretization lazy-propagation tree-algorithms

Posted on Thu, 01 Oct 2026 17:04:06 +0000 by garek007