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])$$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] = raandsize[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!
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.
Invariant & State
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 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;
};
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$.