DSA DSA In Depth
0%
design rollback-dsudynamic-connectivityoffline-queries

Weighted DSU (Potentials/Parity) & Rollback DSU for Dynamic Connectivity Over Time

7 min read Module 5.2: Disjoint Sets, Minimum Spanning Trees & Shortest Paths Updated Oct 23, 2026
📄 Cheat Sheet
Algorithms & Data Structures: Disjoint Set Union (Union-Find, Rank & Rollback)
Pavel Mavrin Algorithms & Data Structures: Disjoint Set Union (Union-Find, Rank & Rollback) Watch on YouTube ↗

Builds on union-find fundamentals to support group potential maintenance along edges and undo stacks.

0:00 / --:--

1. Weighted DSU: Path-Compressed Group Potentials

Suppose each edge $(u, v)$ imposes a constraint $\phi(u) - \phi(v) = w$ in an abelian group $(G, +)$ (or $\phi(u) \oplus \phi(v) = 1$ for 2-coloring / bipartiteness). In a Weighted DSU, every node $x$ stores $\text{diff}[x]$, defined as the group difference between $x$ and its immediate parent $\text{parent}[x]$:

$$\text{diff}[x] = \phi(x) - \phi(\text{parent}[x])$$
Formal Invariant — Telescope Potential Compression & Union Splicing

For any node $x$ with root $r$, the potential of $x$ relative to $r$ is the telescoping sum along the upward path $x = v_0 \to v_1 \to \dots \to v_k = r$:

$$\phi(x) - \phi(r) = \sum_{i=0}^{k-1} \text{diff}[v_i]$$

When path compression rewires $x$ directly to $r$, we first recursively compress $\text{parent}[x]$ (so $\text{diff}[\text{parent}[x]] = \phi(\text{parent}[x]) - \phi(r)$) and then update $\text{diff}[x] \leftarrow \text{diff}[x] + \text{diff}[\text{parent}[x]]$ before setting $\text{parent}[x] = r$.

When merging two components to enforce $\phi(u) - \phi(v) = w$ where $r_u = \text{find}(u)$ and $r_v = \text{find}(v)$, attaching $r_u$ as a child of $r_v$ sets:

$$\text{diff}[r_u] = w + \text{diff}[v] - \text{diff}[u]$$

2. Rollback DSU & Segment Tree Over Time (Offline Dynamic Connectivity)

Why must Rollback DSU disable path compression? A single find(x) with path compression can modify $O(\log N)$ parent pointers, and undoing all of them would require $O(\log N)$ space per query while losing the amortized bound anyway under repeated undo/redo cycles. Using Union by Size without path compression guarantees:

  • Every find(x) is a read-only $O(\log N)$ walk up the tree.
  • Every unite(a, b) mutates at most two array slots (parent[rb] = ra and size[ra] += size[rb]), which can be pushed onto a history stack and rolled back in strict $O(1)$ time!

To handle arbitrary edge additions and deletions over time steps $t \in [0, Q-1]$, each edge $e$ is active on one or more time intervals $[l, r]$. Inserting each interval into a Segment Tree over Time splits $[l, r]$ into $O(\log Q)$ segment tree nodes. A single DFS over the segment tree applies dsu.unite(u, v) upon entering a node and calls dsu.rollback(checkpoint) upon exiting!

Weighted DSU Union Splicing vs. Segment Tree Over Time Rollback Weighted Union: φ(u) - φ(v) = w r_u r_v u v diff[u] diff[v] w + diff[v] - diff[u] Rollback DSU + Time Segment Tree 1. Enter SegTree node: save snapshot() 2. Apply node's edges via unite(u, v) 3. Recurse Left & Right (or answer leaf) 4. Exit node: rollback(snapshot) in O(1)/op
flowchart TD Enter["Enter Segment Tree Node [L, R]"] --> Snap["int snap = dsu.snapshot()"] Snap --> Apply["For each edge (u, v) covering [L, R]: dsu.unite(u, v)"] Apply --> Leaf{"L == R?"} Leaf -- "Yes" --> Ans["Record answer for query at time L"] Leaf -- "No" --> Rec["DFS Left [L, M], then DFS Right [M+1, R]"] Ans --> Roll["dsu.rollback(snap)"] Rec --> Roll
DSU Path Compression vs Undo-Stack Rollback & Dynamic Connectivity Tree
O(α(N)) Amortized vs O(log N) Rollback

Contrast standard Disjoint Set Union (Union-by-Rank + Path Compression flattening trees in O(α(N))) against Rollback DSU (Union-by-Rank ONLY + explicit undo stack [(child, oldParent, oldRank)]) walking an offline edge-lifespan segment tree.

Step 1 / 7
Custom Input:
Timeline

Invariant & State

Before find(4): Skewed chain 4 → 3 → 2 → 1 → 0 (depth 4). Calling find(4) walks up to root 0.
p[4]=3 p[3]=2 p[2]=1 p[1]=0 v0 ROOT v1 depth=1 v2 depth=2 v3 depth=3 v4 depth=4
[0]0
[1]0
[2]1
[3]2
[4]3
parent[] Array
[0, 0, 1, 2, 3]
Tree Depth
4 hops
Amortized Cost
O(α(N)) ≤ 4
[Step 1] Before find(4): Skewed chain 4 → 3 → 2 → 1 → 0 (depth 4). Calling find(4) walks up to root 0.
Hardware Reality — Flat Undo Log vs. Persistent Functional Trees

A persistent DSU built on a path-copied functional treap or rope allocates $O(\log N)$ heap nodes per union. A Rollback DSU stores a flat std::vector< pair > history where each successful merge pushes (childRoot, oldParentOrNegSizeOfChild) and restores both the parent and size in 4 instructions on pop.

Complexity Matrix

DSU Variant find(x) unite(u, v) rollback() Primary Use Case
Weighted DSU (with Compression) O(α(N)) O(α(N)) Unsupported Online Bipartite / Difference Constraints
Rollback DSU (Union by Size Only) O(log N) O(log N) O(1) per undo LIFO Undo / Mo's Algorithm on Graphs
SegTree Over Time + Rollback DSU O(log N) O(log Q log N) O(1) per node exit Offline Dynamic Connectivity (Add + Delete)

Production Implementation: Rollback DSU & Weighted Potential DSU

#include <vector>
#include <utility>

class RollbackDSU {
public:
    explicit RollbackDSU(int n) : p(n, -1), comps(n) {}

    int find(int x) const noexcept {
        while (p[x] >= 0) x = p[x];
        return x;
    }

    bool unite(int a, int b) {
        a = find(a);
        b = find(b);
        if (a == b) return false;
        if (p[a] > p[b]) std::swap(a, b); // p[a] is more negative (larger size)
        history.emplace_back(b, p[b]);
        p[a] += p[b];
        p[b] = a;
        --comps;
        return true;
    }

    size_t snapshot() const noexcept { return history.size(); }

    void rollback(size_t targetSize) noexcept {
        while (history.size() > targetSize) {
            auto [b, oldNegSizeB] = history.back();
            history.pop_back();
            int a = p[b];
            p[a] -= oldNegSizeB;
            p[b] = oldNegSizeB;
            ++comps;
        }
    }

    int components() const noexcept { return comps; }

private:
    std::vector<int> p;
    std::vector<std::pair<int, int>> history;
    int comps;
};

class WeightedDSU {
public:
    explicit WeightedDSU(int n) : parent(n, -1), diff(n, 0) {}

    // Returns (root, phi(x) - phi(root))
    std::pair<int, long long> find(int x) {
        if (parent[x] < 0) return {x, 0};
        auto [r, dParent] = find(parent[x]);
        parent[x] = r;
        diff[x] += dParent;
        return {r, diff[x]};
    }

    // Enforces phi(u) - phi(v) == w. Returns false if inconsistent with existing component!
    bool unite(int u, int v, long long w) {
        auto [ru, du] = find(u);
        auto [rv, dv] = find(v);
        if (ru == rv) return (du - dv) == w;
        if (parent[ru] > parent[rv]) {
            parent[rv] += parent[ru];
            parent[ru] = rv;
            diff[ru] = w + dv - du;
        } else {
            parent[ru] += parent[rv];
            parent[rv] = ru;
            diff[rv] = du - dv - w;
        }
        return true;
    }

private:
    std::vector<int> parent;
    std::vector<long long> diff;
};
Failure Mode — Non-LIFO Undo Attempts & Unclosed Edge Intervals

A Rollback DSU stack only supports LIFO (stack-ordered) rollbacks; if edge $e_1$ is added before $e_2$, you cannot delete $e_1$ while keeping $e_2$ without the Segment Tree over Time wrapper! Also, when building the Segment Tree over Time, remember to close all edges still present at the end of the query stream with right endpoint $R = Q - 1$.

Check yourself Question 1 of 4
Concept Check Why Path Compression Breaks Rollback DSU recall
Section: Rollback DSU Invariant

Why must a Rollback DSU omit Path Compression and rely solely on Union by Size/Rank?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

In an online bipartite graph checker using Parity DSU (where `parity[x]` is the XOR distance to `find(x)`), suppose `unite(u, v)` finds that `u` and `v` already share the same root `r`. Under what condition does adding edge $(u, v)$ create an odd cycle?

Key Takeaways

Weighted DSU stores diff[x] = phi(x) - phi(parent[x]) and accumulates telescoping sums during path compression to maintain group potentials or bipartite parity in O(alpha(N)) time.
Rollback DSU disables path compression and relies exclusively on Union by Size so that find(x) is O(log N) read-only and unite(u, v) mutates only 2 integers.
Restoring a Rollback DSU to any prior snapshot pops the history stack in O(1) per rolled-back union.
Splitting each edge's active lifespan [L, R] across a Segment Tree over Time turns arbitrary edge additions and deletions into nested LIFO scopes solvable in O((M + Q) log Q log N).
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue