Union-Find (DSU) Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide
Keep groups of connected items; "union" merges two groups and "find" tells you which group an item is in, almost in O(1).
4 sub-patterns Β· 16 questions
What is this pattern?
Union-Find (also called Disjoint Set Union, DSU) keeps items split into groups and supports two operations: find(x) tells you which group x is in, and union(a, b) merges the groups of a and b. Each group is stored as a small tree: every item points to a parent, and the item at the top (the root) is the group's name. Two items are in the same group exactly when find gives the same root.
Two small tricks keep the trees almost flat. Union by size: hang the smaller tree under the bigger one, so a tree can only get one level deeper when its size at least doubles. Path compression: while find walks up, re-point the nodes it passes closer to the root. Together each operation costs almost O(1) (the inverse Ackermann function, below 5 for any real input).
Use it when edges arrive one at a time and you keep asking "are these two connected now?" A BFS would have to start over after every new edge; Union-Find just merges two roots. union returning false (both ends already share a root) is the classic cycle / redundant edge signal, and sorting edges by cost and keeping only the ones that join two groups builds a minimum spanning tree (Kruskal).
Companies buying companies. Every employee knows their boss, and the boss at the top (the CEO) names the company. To check whether two people work for the same company, each walks up to their CEO and you compare. When two companies merge, the smaller company's CEO starts reporting to the bigger company's CEO: one change, and everyone below follows. Path compression is an employee who, after walking up once, writes down the CEO's name so next time the walk is short.
See it move
Arrows point to the parent; roots are on top. Purple = the two roots compared, green = the link just made, red = same root (redundant edge).
n = 7, unions: (0,1) (2,3) (1,3) (4,5) (5,6) (3,6) (0,6)step 1/15- groups
- 7
parent[i] = i means i is a root, the name of its group. size is kept only on roots (Β· elsewhere). Purple = the two roots being compared, green = the edge just added.
Gold = in the spanning tree, red = skipped (would close a cycle), blue = being checked. The note under each node is its current root.
n = 6, edges (u, v, w): (0,1,4) (0,2,3) (1,2,1) (1,3,2) (2,3,2) (3,4,2) (4,5,6) (3,5,3) (2,4,5)step 1/8- total
- 0
- edges used
- 0 of 5
Kruskal looks at edges from cheapest to most expensive. Each node starts as its own group (root = itself). Gold = edge in the tree, red = skipped, blue = the edge being checked.
How do I recognise it?
"Are a and b connected?" asked again and again while connections are being added.
"Number of connected components / groups / provinces", especially from an edge list.
"Which edge can be removed so the graph becomes a tree?", "redundant connection", "does this edge make a cycle?" in an undirected graph.
Items that are equal / linked / swappable: "a == b", "same email", "can swap indices i and j", "similar strings" β merge, then handle each group.
"Minimum cost to connect all points / cities" β minimum spanning tree (Kruskal).
Grid cells or stones that join when they share a row, a column or a side.
Looks like Union-Find (DSU), but isn't
Union-Find only knows whether two items are connected, not how far apart they are. It throws the path away.
Union-Find ignores direction. 0β1, 0β2, 1β2 has no directed cycle, but Union-Find sees 1 and 2 already joined and reports one.
If the graph is fully known and asked about once, one DFS is simpler. Union-Find wins when edges arrive over time or groups must be merged by rules.
Union-Find can merge groups but cannot split them. A common trick is to run time backwards so removals become unions.
How would I discover it from scratch?
Don't memorise βuse union-find (dsu)β. Ask these questions in order β the pattern falls out.
Keeping a list of members per group makes merging cost the size of a group. Instead let every item point to a parent, and let the root name the group. Merging two groups is now one assignment: parent[rootB] = rootA.
Sub-patterns
Same core idea, different rule for βvalidβ. Learn them in this order.
Connectivity / components
5 questionsCount or test the groups that the given connections form.
Cycle detection
4 questionsFind the edge that closes a cycle, or count the edges that are spare.
Grouping / equivalence
4 questionsMerge items that are declared equal (or connected by a rule), then process each group together.
Minimum spanning tree (Kruskal)
3 questionsConnect all nodes with the smallest total edge cost.
Don't mix these up
Same answers on a fixed graph. Union-Find shines when edges arrive one by one with questions in between, or when merging follows rules (rows with columns, equal letters). BFS/DFS is simpler for one question on a known graph.
Two items with different parent can still be in one group. Always compare find(a) == find(b), and always link roots: parent[find(b)] = find(a), never parent[b] = a.
Union-Find doesn't see arrows. Use it for undirected edges; for prerequisites and other directed edges, use Kahn or three-colour DFS.
Both build an MST. Kruskal is natural with an edge list. Prim fits a dense graph where every pair is an edge (all points), where its O(nΒ²) array version beats sorting nΒ² edges.
Code templates (C++)
DSU with path compression and union by size
The one struct every question here starts from. unite returns false when both ends already share a root, and groups counts components as you go.
// Union-Find (Disjoint Set Union) with path compression + union by size.
struct DSU {
vector<int> parent, sz;
int groups;
DSU(int n) : parent(n), sz(n, 1), groups(n) { iota(parent.begin(), parent.end(), 0); }
int find(int x) { // the root that names x's group
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // path compression (halving): skip a level each step
x = parent[x];
}
return x;
}
bool unite(int a, int b) { // false when a and b were already in one group
a = find(a), b = find(b);
if (a == b) return false;
if (sz[a] < sz[b]) swap(a, b); // hang the smaller tree under the bigger one
parent[b] = a;
sz[a] += sz[b];
groups--;
return true;
}
};Cycle / redundant edge
The first edge whose unite fails is the redundant one. For Network Connected, each failed unite is a spare cable and groups β 1 cables are needed.
// Undirected cycle / redundant edge: an edge whose two ends already share a root closes a cycle.
// Uses the DSU from the Union-Find template.
vector<int> firstRedundantEdge(int n, const vector<vector<int>>& edges) { // nodes 1..n
DSU dsu(n + 1);
for (auto& e : edges)
if (!dsu.unite(e[0], e[1])) return e; // already connected: this edge makes a cycle
return {};
}
// How many cables to move so all n computers connect (Network Connected):
// spare edges = edges that closed a cycle; you need (groups - 1) of them.
int makeConnected(int n, const vector<vector<int>>& connections) {
if ((int)connections.size() < n - 1) return -1;
DSU dsu(n);
for (auto& c : connections) dsu.unite(c[0], c[1]);
return dsu.groups - 1;
}Grouping equivalent items
Union every given pair, then collect each root's members and work on the group as a whole (here: sort each group's letters). Accounts Merge and Equality Equations follow the same two phases.
// Grouping by "is equivalent to": union every given pair, then collect each root's members.
// Uses the DSU from the Union-Find template. Here: Smallest String With Swaps.
string smallestStringWithSwaps(string s, const vector<vector<int>>& pairs) {
int n = s.size();
DSU dsu(n);
for (auto& p : pairs) dsu.unite(p[0], p[1]); // swaps chain: (0,3) and (3,1) let 0, 1, 3 trade freely
unordered_map<int, vector<int>> members; // root β indices in that group, increasing
for (int i = 0; i < n; i++) members[dsu.find(i)].push_back(i);
for (auto& [root, idx] : members) {
string chars;
for (int i : idx) chars += s[i];
sort(chars.begin(), chars.end()); // inside a group any arrangement is reachable
for (int k = 0; k < (int)idx.size(); k++) s[idx[k]] = chars[k];
}
return s;
}Kruskal's minimum spanning tree
Sort edges by weight, keep an edge only if unite succeeds, stop at n β 1 edges. Fewer than n β 1 kept β the graph is not connected.
// Kruskal's minimum spanning tree: cheapest edges first, skip any edge inside one group.
// Uses the DSU from the Union-Find template. Returns -1 if the graph is not connected.
long long kruskal(int n, vector<array<int, 3>> edges) { // {weight, u, v}, nodes 0..n-1
sort(edges.begin(), edges.end());
DSU dsu(n);
long long total = 0;
int used = 0;
for (auto& [w, u, v] : edges) {
if (!dsu.unite(u, v)) continue; // would close a cycle: never needed
total += w;
if (++used == n - 1) break; // a spanning tree has exactly n - 1 edges
}
return used == n - 1 ? total : -1;
}