A persistent structure keeps every historical version accessible. The trick is path copying: an update copies only the nodes on the path it modifies, sharing everything else with the previous version.

Persistent segment tree

struct Node { int l = 0, r = 0; long long sum = 0; };
vector<Node> t{Node{}};                              // node 0 = the null node
vector<int> root;                                    // root[v] = version v
 
int build(int l, int r) {
    int cur = t.size(); t.push_back({});
    if (l == r) return cur;
    int m = (l + r) / 2;
    t[cur].l = build(l, m);
    t[cur].r = build(m + 1, r);
    return cur;
}
 
int update(int prev, int l, int r, int pos, long long val) {
    int cur = t.size(); t.push_back(t[prev]);        // COPY the old node
    if (l == r) { t[cur].sum += val; return cur; }
    int m = (l + r) / 2;
    if (pos <= m) t[cur].l = update(t[prev].l, l, m, pos, val);
    else          t[cur].r = update(t[prev].r, m + 1, r, pos, val);
    t[cur].sum = t[t[cur].l].sum + t[t[cur].r].sum;
    return cur;
}
 
long long query(int node, int l, int r, int ql, int qr) {
    if (qr < l || r < ql || !node) return 0;
    if (ql <= l && r <= qr) return t[node].sum;
    int m = (l + r) / 2;
    return query(t[node].l, l, m, ql, qr) + query(t[node].r, m + 1, r, ql, qr);
}

time and new nodes per update. Total memory .

The killer application: -th smallest in a range

Build version by inserting into version , indexed by value. Then version minus version is a segment tree over the values present in — and descending it gives the -th smallest in .

int kth(int u, int v, int l, int r, int k) {        // u = root[l-1], v = root[r]
    if (l == r) return l;
    int leftCount = t[t[v].l].sum - t[t[u].l].sum;
    int m = (l + r) / 2;
    if (k <= leftCount) return kth(t[u].l, t[v].l, l, m, k);
    return kth(t[u].r, t[v].r, m + 1, r, k - leftCount);
}

Subtracting two versions is the central idea — it works because the trees have identical shape, so corresponding nodes can be walked in lockstep. This pattern also gives:

  • count of elements in ,
  • number of distinct values in (with a “last occurrence” formulation),
  • sum of the smallest in ,
  • range mode and quantile queries.

The persistent family

StructureUpdateMemory per updateUse
Persistent segment treerange -th, historical queries
Persistent arraythe primitive under everything else
Persistent triemax XOR over an index range
Persistent treap am.versioned sequences with insert/erase
Persistent DSUconnectivity at any point in history
Persistent heapEppstein’s k shortest paths

Full vs partial persistence

  • Partial persistence — old versions are readable but only the newest is writable. This is what path copying gives, and it covers essentially every contest use.
  • Full persistence — any version can be updated, producing a version tree. Also achievable with path copying; you just keep a root per version and never assume a linear order.

Memory

The binding constraint. nodes at 12-16 bytes each: for and , that is nodes ≈ 100 MB. Mitigations:

  • Skip the initial build — start from an empty tree with implicit null children (node 0).
  • Use int for child indices, not pointers.
  • Preallocate the node array with reserve to avoid reallocation.
  • Store only what you need in each node.

When to prefer something else

  • Queries known in advance → an offline BIT sweep is memory and faster.
  • Only the latest version matters → an ordinary segment tree.
  • Memory is very tight and you need range -th → a wavelet tree uses bits instead of integers.

See also: Segment Tree · Wavelet Tree · K-th Order Statistics