Connectivity under edge insertions and deletions.

OperationsTechniqueCost
insertions onlyDSU
deletions onlyreverse time, then DSU
both, offlinesegment tree over time + rollback DSU
both, online, graph stays a forestlink-cut tree (LCT)
both, online, generalHolm–de Lichtenberg–Thorup (HDT) amortised
offline, with a maximum spanning forest by deletion timelink-cut tree keyed by deletion time
both, online, with MST maintenanceHDT extended with edge weights amortised

Offline: segment tree over time

Each edge is alive on a set of time intervals. Insert each interval into the segment-tree nodes covering it; DFS the tree, uniting a node’s edges on the way down and rolling them back on the way up. At leaf the DSU contains exactly the edges alive at time . The DSU uses union by size and no path compression, so each union is undone in .

struct RollbackDSU {
    vector<int> p, s; vector<pair<int, int>> hist; int comps;
    RollbackDSU(int n) : p(n), s(n, 1), comps(n) { iota(all(p), 0); }
    int find(int v) const { while (p[v] != v) v = p[v]; return v; }
    bool unite(int a, int b) {
        a = find(a), b = find(b);
        if (a == b) { hist.push_back({-1, -1}); return false; }
        if (s[a] < s[b]) swap(a, b);
        hist.push_back({b, a}); p[b] = a; s[a] += s[b]; comps--; return true;
    }
    void undo() { auto [b, a] = hist.back(); hist.pop_back(); if (b >= 0) p[b] = b, s[a] -= s[b], comps++; }
};
struct OfflineConnectivity {                              // times 0..T-1
    int T; vector<vector<pair<int, int>>> seg;
    OfflineConnectivity(int T) : T(T), seg(4 * max(T, 1)) {}
    void addEdge(int l, int r, pair<int, int> e) { add(1, 0, T, l, r, e); }   // edge alive during [l, r)
    void add(int x, int lo, int hi, int l, int r, pair<int, int> e) {
        if (r <= lo || hi <= l) return;
        if (l <= lo && hi <= r) { seg[x].push_back(e); return; }
        int m = (lo + hi) / 2; add(2 * x, lo, m, l, r, e); add(2 * x + 1, m, hi, l, r, e);
    }
    template<class F> void run(RollbackDSU& d, F atLeaf, int x = 1, int lo = 0, int hi = -1) {
        if (hi < 0) hi = T;
        for (auto [u, v] : seg[x]) d.unite(u, v);
        if (hi - lo == 1) atLeaf(lo);
        else { int m = (lo + hi) / 2; run(d, atLeaf, 2 * x, lo, m); run(d, atLeaf, 2 * x + 1, m, hi); }
        for (int i = 0; i < sz(seg[x]); i++) d.undo();
    }
};
// lifetimes: map<pair<int,int>, int> start; on add: start[e] = t; on remove: addEdge(start[e], t, e), erase; at the end close the rest at T

Anything a DSU maintains reversibly can be answered at the leaves: component count, sizes, sums per component, bipartiteness (parity DSU), “number of edges inside each component”, and even a linear basis or a knapsack per component if it supports undo.

Offline via maximum spanning forest by deletion time

Process time forward and maintain a spanning forest that is maximal with respect to deletion time: when inserting edge that closes a cycle, find the cycle edge with the earliest deletion time (path minimum in a link-cut tree), and if it is earlier than ‘s, replace it with . Then a tree edge’s deletion never has a replacement among non-tree edges (they all die earlier), so connectivity queries are answered by the forest. with an LCT.

Online: Holm–de Lichtenberg–Thorup

Each edge has a level ; = spanning forest of edges with level , with the invariants and “every component of has at most vertices”. Forests are kept as Euler tour trees (ETT) that can report a non-tree edge incident to a component.

  • Insert: level 0; tree edge if it connects two components of , else non-tree.
  • Delete a tree edge of level : for : take the smaller of the two split trees in , raise all its level- tree edges to , then scan its level- non-tree edges: one reaching the other side is the replacement (stop); one staying inside is raised to .
  • Query: same tree of .

In contests, the offline method is almost always enough; HDT is worth knowing for online problems and for dynamic MST / 2-edge-connectivity extensions (which add more levels of bookkeeping — dynamic bipartiteness is the same level machinery with a parity bit per level instead of just a spanning forest). Frederickson’s earlier structure gets worst-case (not just amortised) update time with a simpler sparsification-tree idea, predating HDT; HDT is asymptotically better and what’s actually implemented in practice.