Dynamic (implicit) segment trees create nodes only when an update touches them, so the coordinate range can be with memory. Persistent trees copy the touched path so every version survives. Both use index-based nodes, and both pair naturally with tag permanence (tags never pushed down).

Dynamic segment tree (range add, range sum)

struct DynSeg {
    ll lo, hi; vector<array<int, 2>> ch{{0, 0}}; vector<ll> sum{0}, tag{0};   // node 0 = root
    DynSeg(ll lo, ll hi) : lo(lo), hi(hi) {}
    int child(int p, int c) {
        if (!ch[p][c]) { ch[p][c] = sz(ch); ch.push_back({0, 0}); sum.push_back(0); tag.push_back(0); }
        return ch[p][c];
    }
    void add(int p, ll l, ll r, ll x, ll y, ll v) {    // [x, y) within node [l, r)
        x = max(x, l); y = min(y, r); if (x >= y) return;
        sum[p] += v * (y - x);
        if (x == l && y == r) { tag[p] += v; return; }
        ll m = l + (r - l) / 2;
        if (x < m) { int c = child(p, 0); add(c, l, m, x, y, v); }
        if (m < y) { int c = child(p, 1); add(c, m, r, x, y, v); }
    }
    ll query(int p, ll l, ll r, ll x, ll y) const {
        x = max(x, l); y = min(y, r); if (x >= y) return 0;
        if (x == l && y == r) return sum[p];
        ll m = l + (r - l) / 2, res = tag[p] * (y - x);
        if (ch[p][0]) res += query(ch[p][0], l, m, x, y);
        if (ch[p][1]) res += query(ch[p][1], m, r, x, y);
        return res;
    }
    void add(ll x, ll y, ll v) { add(0, lo, hi, x, y, v); }
    ll query(ll x, ll y) const { return query(0, lo, hi, x, y); }
};
  • Each update creates at most nodes. Reserve vectors if the count is predictable.
  • Negative coordinates are fine; use l + (r - l) / 2 for the midpoint.
  • Take the child index into a local before recursing (push_back may reallocate).
  • If all coordinates are known offline, compress them instead: faster and less memory.
  • 2D dynamic tree (point add, rectangle sum, online): an outer dynamic tree over , each node owning a dynamic inner tree over ; per operation and memory per point. Offline, a compressed 2D Fenwick is about 10x lighter.

Persistent segment tree

struct PSeg {
    int n; vector<int> L{0}, R{0}; vector<ll> S{0};    // node 0 = empty tree, children are itself
    PSeg(int n) : n(n) {}
    int add(int p, int l, int r, int pos, ll v) {
        int q = sz(L); L.push_back(L[p]); R.push_back(R[p]); S.push_back(S[p] + v);
        if (r - l > 1) {
            int m = (l + r) / 2;
            if (pos < m) { int c = add(L[p], l, m, pos, v); L[q] = c; }
            else { int c = add(R[p], m, r, pos, v); R[q] = c; }
        }
        return q;
    }
    ll sum(int p, int l, int r, int x, int y) const {
        if (!p || y <= l || r <= x) return 0;
        if (x <= l && r <= y) return S[p];
        int m = (l + r) / 2;
        return sum(L[p], l, m, x, y) + sum(R[p], m, r, x, y);
    }
    int kth(int a, int b, ll k) const {                // k-th (0-indexed) of version b minus version a
        int l = 0, r = n;
        while (r - l > 1) {
            int m = (l + r) / 2; ll c = S[L[b]] - S[L[a]];
            if (k < c) a = L[a], b = L[b], r = m; else k -= c, a = R[a], b = R[b], l = m;
        }
        return l;
    }
};

Persistent lazy (range add): with tag permanence, an update copies only the nodes it visits and adds the tag to fully covered copies; queries accumulate tag * overlap along the way. “Go back to version ” is just roots.push_back(roots[t]) (SPOJ TTM “To the moon”). With push-down tags you’d have to copy children on every push, which doubles memory but works for non-commutative tags.

Point assign on a sum-only tree: there’s no separate “set” operation above, but one is free: add(root, pos, v - sum(root, pos, pos + 1)) adds exactly the difference needed to make the point equal . For -th-smallest-by-value queries on a static array, a wavelet matrix answers the same questions with less memory and no persistence machinery — reach for it instead when every version isn’t actually needed.

What they solve

ProblemTool
-th smallest / count in value range of , onlinepersistent tree over values, versions = prefixes
number of distinct values in , onlinepersistent tree over positions keeping only last occurrences
queries on old versions after range updatespersistent lazy tree
point updates on a linedynamic tree
-th smallest on a tree pathpersistent tree versions along root paths:
“how many of the first updates affect position “persistence over the update sequence
merging and splitting sets of valuesdynamic trees with merge/split (Merging and Splitting)

Memory: about (updates) nodes. At updates and that’s 4 million nodes, fine. Persistent lazy trees with large ranges can need 50+ million nodes, so estimate first.