Union-Find & Minimum Spanning Trees
Merge groups and ask "same group?" in near-constant time — then use it to wire a graph as cheaply as possible.
01 · why it exists
Why this exists, and the weight it carries
since1964 (analysed 1973–75)
Union-Find (Disjoint Set Union)
Bernard A. Galler & Michael J. Fischer; analysis by John Hopcroft & Jeffrey Ullman, and Robert Tarjan
In 1964 Galler and Fischer published a three-page CACM paper on a compiler chore: FORTRAN's EQUIVALENCE statements say which variables share memory, and the compiler must group them. Their idea was to store each group as a tree, finding every group in one scan of the declarations — cutting computation time by 40% compared with the method published before it. Why it is so fast took another decade: Hopcroft and Ullman (1973) proved an O(log* n) bound, and Tarjan (1975) pinned it to the inverse Ackermann function α(n). In 1989 Fredman and Saks proved that, in the cell-probe model, no structure can beat α(n) amortized per operation.
the problem it solvedGrouping things that are declared equal pair by pair, and answering "same group?" without re-scanning everything.
its jobs today
- Kruskal's MST: for every candidate edge the DSU answers "would this close a cycle?"
- Compilers: grouping things known to be equal — FORTRAN EQUIVALENCE storage was its first job; unification in Hindley–Milner type inference is a modern one.
- Image processing: connected-component labelling merges touching pixel labels — OpenCV's
connectedComponentsoffers SAUF, a scan + array-based union-find algorithm. - Identity resolution: merging user records that share a phone number or email (Accounts Merge at scale).
the weight it carries
Illustrative: 10 lakh (1,000,000) user records and 20 lakh "same person" links, with a "same person?" check after every link. A fresh BFS each time touches up to 3·10⁶ nodes + links: 2·10⁶ × 3·10⁶ = 6·10¹² steps. Union-find with both tricks: 2 finds per link, a handful of hops each (α(10⁶) ≤ 4): about 2·10⁶ × 2 × 4 = 1.6·10⁷ hops. Union by size alone already caps every tree's height at ⌊log₂ 10⁶⌋ = 19.
without it
Every "are these connected yet?" question would mean a fresh BFS/DFS over the whole graph — fine once, hopeless after each of millions of updates.
when · why · how
- when
- Pairs arrive one by one (edges,
a == b, shared emails) and you need group membership, group counts or "does this edge close a cycle?" in between. - why
- With union by size + path compression, m operations cost O(m·α(n)) — effectively constant each. The price: it only merges, it can never split a group.
- how
int[] parent(parent[i] = i) +int[] size; recursivefindthat re-points nodes at the root;unionreturns false when the roots already match. For string keys, aHashMap<String, String>parent map.
since1926
Minimum spanning tree (and the cut property)
Otakar Borůvka (asked by Jindřich Saxel of West-Moravian Powerplants)
In winter 1925/26, Jindřich Saxel, an employee of the West-Moravian Powerplants, suggested a problem to the mathematician Otakar Borůvka from the electrification of south-west Moravia: connect a set of towns with the least total length of line. In 1926 Borůvka published his answer twice — a 22-page paper in a Brno journal and a one-page note in an electrical-engineering magazine, worked on 40 points. His method: every piece joins its nearest neighbour, the pieces merge, repeat. Historians of the problem credit him with the exchange argument at the heart of every greedy MST algorithm.
the problem it solvedConnecting every site of a network — power lines, cables, roads — with the smallest possible total length.
its jobs today
- Network design: the cheapest set of fibre, power or pipe links that still connects every site.
- Clustering: build the MST, delete the k − 1 heaviest edges, and the pieces left are k clusters (single linkage).
- Approximation: walking around an MST (every edge twice) and skipping cities already visited gives a tour at most twice the optimum for the metric travelling-salesman problem.
the weight it carries
Illustrative: 1,000 towns where any two could be linked: 1,000 × 999 / 2 = 4,99,500 possible links. A spanning tree uses exactly 999 of them, and Cayley's formula counts 1000⁹⁹⁸ different spanning trees — trying them all is impossible. The cut property lets a greedy algorithm pick the right 999 with one sort (Kruskal) or 1,000 × 1,000 = 10⁶ steps (dense Prim).
without it
The only exact method would be to compare an astronomically large number of trees; network planners would be guessing.
when · why · how
- when
- "Connect everything at minimum total cost", "exactly one path between every pair", or grouping points by closeness.
- why
- Cut property: the cheapest edge crossing any split of the nodes is safe to take, so greedy choices are never wrong — O(E log E) instead of exponential.
- how
- Kruskal (sort
int[]{u, v, w}+ DSU) when you have an edge list; Prim (PriorityQueueor abest[]array) for adjacency lists and dense graphs. Expect exactly n − 1 edges.
since1956
Kruskal's algorithm
Joseph B. Kruskal
Probably a few months after his 1954 PhD, someone at Princeton handed Kruskal two flimsy typewritten pages in German that were "floating around" the maths department: an abstract of Borůvka's 1926 paper. He found the method elegant but unnecessarily complicated, and boiled it down: take the shortest remaining edge that doesn't close a cycle; repeat. He wasn't sure something so simple deserved publishing, and was advised to go ahead. The three-page 1956 note became one of his best-known papers, as he recalled in 1997.
the problem it solvedA simple, provably correct way to build the shortest spanning tree — and to show it is unique when all lengths differ.
its jobs today
- Network design straight from a list of candidate links — sort + DSU, no adjacency list needed.
- Image segmentation: Felzenszwalb–Huttenlocher processes pixel edges in increasing weight and merges regions with union-find — Kruskal's loop with a smarter merge test.
- Single-linkage clustering: stop Kruskal when k components are left.
the weight it carries
Illustrative: 10,000 candidate cable routes between 2,000 offices. Sorting: about 10,000 × ⌈log₂ 10,000⌉ = 10,000 × 14 = 1.4 lakh comparisons, then 10,000 near-O(1) DSU checks — and it can stop after 1,999 accepted links. Checking each link for a cycle with a fresh DFS instead: up to 10,000 × ~6,000 = 6·10⁷ steps.
without it
Every edge-list MST would need an adjacency list and a heap (Prim), or a slow DFS cycle check per edge.
when · why · how
- when
- The input is an edge list (
[u, v, w]), the graph is sparse, or you want the components as they form (clustering). - why
- Sorting dominates: O(E log E); the DSU adds only O(E·α(V)). The cut property makes "cheapest safe edge first" correct.
- how
Arrays.sort(edges, Comparator.comparingInt(e -> e[2])), then for each edge:if (union(u, v)) { total += w; if (++used == n - 1) break; }.
since1930 · 1957 · 1959
Prim's algorithm (Jarník–Prim)
Vojtěch Jarník; Robert C. Prim (Bell Labs); Edsger W. Dijkstra
In 1930 Jarník answered Borůvka's paper with a note "from a letter to O. Borůvka", describing a different method: grow one tree, always adding the shortest edge that leaves it. In 1957 Robert Prim of Bell Labs published the same tree-growing method; his paper says the problem matters for planning communication networks and for the Bell System's leased-line rates. Dijkstra published it again in 1959, in the same short note as his shortest-path algorithm — so it is also called the Jarník–Prim (or DJP) algorithm.
the problem it solvedBuilding a minimum spanning tree by growing a single tree outward — no global sort of all edges needed.
its jobs today
- Dense networks where every pair can be linked (points on a map, LC 1584): the O(V²) array version never builds an edge list.
- Adjacency-list graphs: lazy Prim with a
PriorityQueue— the same shape as Dijkstra, with "edge weight" in place of "distance so far".
the weight it carries
Illustrative: 1,000 delivery points where any two can be linked. Dense Prim: 1,000 rounds × 1,000 scans = 10⁶ steps and two arrays of 1,000. Kruskal would first build and sort 4,99,500 edges: about 4,99,500 × ⌈log₂ 4,99,500⌉ = 4,99,500 × 19 ≈ 95 lakh comparisons, plus ~6 MB of raw ints (3 per edge, 4 bytes each).
without it
Dense graphs would force you to materialise all n²/2 pairs before choosing a single link.
when · why · how
- when
- You have an adjacency list, or the graph is complete/dense (every pair has a cost).
- why
- Lazy heap version: O(E log E), skipping stale entries. Array version: O(V²) — better than E log E once E ≈ V².
- how
PriorityQueue<int[]>withComparator.comparingInt(a -> a[1])+boolean[] inTree; or anint[] bestarray and a linear scan per round.
02 · plain english
The idea in 30 seconds
Union-Find (a Disjoint Set Union, DSU) keeps elements in groups and answers two questions fast: which group is x in? and merge these two groups.
Each group is a small tree stored in a parent[] array; the root is the group's name. Two tricks — union by size and path compression — keep the trees almost flat.
A minimum spanning tree (MST) connects every node of a weighted graph with the least total weight. Kruskal = cheapest edges first, skipping any that close a cycle (DSU spots them). Prim = grow one tree with a min-heap.
picture this
Your phone's contacts app merging duplicates. Every contact card points to a "main" card, and the main card is the person. "Same person?" = follow both cards to their main cards and compare. Merging two people = one main card starts pointing at the other. Path compression = after one long lookup, every card you passed now points straight at the main card, so the next lookup is one hop.
03 · remember
What you must know
parent[i]points one step up; a root hasparent[r] == r. Two elements are in the same set exactly when they have the same root.- find(x) walks up to the root. union(a, b) finds both roots and hangs one under the other. Everything else is built from these two.
- Union by size (or rank): hang the smaller tree under the bigger root, so height stays ≤ log₂ n. Path compression: during find, re-point every visited node straight at the root.
- With both tricks, any sequence of m operations costs O(m·α(n)). α is the inverse Ackermann function — α(n) ≤ 4 for any n you will ever meet, so treat each operation as amortized constant time.
- Counting components: start with
count = n; every union that actually merges two sets doescount--. Cycle detection (undirected): an edge whose two ends already share a root closes a cycle. - Cut property — the reason greedy MST works: split the nodes into any two groups; the cheapest edge crossing between them is safe to put in some MST.
- Kruskal: sort edges (O(E log E)), then take each edge whose ends are in different DSU sets. Prim: start anywhere, repeatedly add the cheapest edge leaving the tree (PriorityQueue, O(E log V)); an O(V²) array version wins on dense/complete graphs.
- A spanning tree of n nodes has exactly n − 1 edges. Fewer usable edges means the graph is disconnected — there is no spanning tree.
04 · see it
Play with it in 3D
Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.
Union-Find forest — union by size + path compression
Top: the forest (arrows point to parent). Below: the same thing as `parent[]` and `size[]`. Watch nodes re-point at their root during find.
Cycle detection — the edge whose ends are already connected
Edges arrive one at a time. Same root = there is already a path, so this edge closes a cycle (red).
Kruskal — cheapest edge first, skip cycles
Lime = in the tree, grey dashed = rejected because it would close a cycle. The DSU answers "already connected?" instantly.
Prim — one tree grows, a min-heap feeds it
Amber edges are offers in the heap. Some go stale when their node joins by another edge that is no more expensive — they are skipped when polled.
05 · cost
Time & space, explained
| find / union | Space | why | |
|---|---|---|---|
| Naive quick-union (no tricks) | O(n) worst | O(n) | Unions can build a chain like 0←1←2←…; find from the bottom walks the whole chain. |
| Union by size or rank only | O(log n) worst | O(n) | A node only gets deeper when its tree joins one at least as big, so its tree size doubles each time — at most log₂ n times. |
| Path compression only | O(log n) amortized | O(n) | Long paths are flattened the first time they are walked, so expensive finds pay for cheap future ones (Tarjan & van Leeuwen). |
| Both tricks | O(α(n)) amortized | O(n) | Tarjan's bound: m operations cost O(m·α(n)), and α(n) ≤ 4 in practice — effectively constant. |
| HashMap-based DSU (any keys) | O(α(n)) amortized, average | O(n) | Same algorithm with both tricks; each parent lookup is an average O(1) hash operation instead of an array index. (The short map version shown uses path compression only: O(log n) amortized.) |
| Time | Extra space | why | |
|---|---|---|---|
| Kruskal (sort + DSU) | O(E log E) = O(E log V) | O(V + E) | Sorting dominates; the E DSU operations add only O(E·α(V)). log E ≤ 2 log V because E ≤ V². |
| Prim, lazy (PriorityQueue of edges) | O(E log E) = O(E log V) | O(E) | Each edge is offered at most once (when the first of its ends joins the tree), so ≤ E offers and polls, each O(log E). |
| Prim, dense (array, no heap) | O(V²) | O(V) | V rounds, each scanning all V nodes for the cheapest. Best when E ≈ V², e.g. complete graphs of points. |
| Prim, Fibonacci heap (theory) | O(E + V log V) | O(V) | decrease-key is O(1) amortized. Rarely worth it in practice — and Java has no Fibonacci heap. |
| Brute force: try every spanning tree | Exponential | — | A complete graph has V^(V−2) spanning trees (Cayley's formula). Greedy + cut property avoids all of that. |
06 · java
Raw vs Collections Framework
The interview DSU: arrays, union by size, path compression
class DSU { private final int[] parent; private final int[] size; private int components; DSU(int n) { parent = new int[n]; size = new int[n]; components = n; for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; } } /** Root of x's set. Path compression: every node on the way up is re-pointed at the root. */ int find(int x) { if (parent[x] != x) { parent[x] = find(parent[x]); } return parent[x]; } /** Merge the sets of a and b. Returns false if they were already together. */ boolean union(int a, int b) { int ra = find(a), rb = find(b); if (ra == rb) return false; if (size[ra] < size[rb]) { int t = ra; ra = rb; rb = t; } parent[rb] = ra; // smaller tree hangs under the bigger root size[ra] += size[rb]; components--; return true; } boolean connected(int a, int b) { return find(a) == find(b); } int components() { return components; }}class MapDSU<T> { private final Map<T, T> parent = new HashMap<>(); /** Unseen keys are created on demand as their own root. */ T find(T x) { parent.putIfAbsent(x, x); T p = parent.get(x); if (!p.equals(x)) { p = find(p); parent.put(x, p); // path compression } return p; } boolean union(T a, T b) { T ra = find(a), rb = find(b); if (ra.equals(rb)) return false; parent.put(ra, rb); return true; }}- Java has no built-in union-find — you write it. It is ~20 lines; practise until you can type it from memory in 2 minutes.
- Use
int[]when elements are already 0..n−1 (or 1..n — allocate n + 1). It is the fastest and the clearest. - Use a
HashMap<T, T>when elements are strings, coordinates or sparse numbers (emails,x * C + y, values up to 10⁹).putIfAbsentcreates singleton sets lazily. - Return a
booleanfromunion:falsemeans "already connected" — that one bit gives you cycle detection and redundant-edge detection for free. - Recursive find is fine with union by size (depth ≤ log₂ n). Without balancing, deep chains can overflow the stack — use the iterative version.
Why the tricks matter: naive quick-union
class NaiveUnionFind { private final int[] parent; NaiveUnionFind(int n) { parent = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; // everyone starts as their own root } /** Walk up until a node is its own parent. O(height) — can be O(n) on a chain. */ int find(int x) { while (parent[x] != x) x = parent[x]; return x; } /** Hang one root under the other, with no thought for balance. */ boolean union(int a, int b) { int ra = find(a), rb = find(b); if (ra == rb) return false; parent[ra] = rb; return true; }}- Correct, but
union(0,1), union(1,2), union(2,3)…builds a chain, and every find becomes O(n). - Interviewers often ask you to start here and then improve it — say which trick fixes which problem.
Variation: union by rank + iterative path halving
class RankDSU { private final int[] parent; private final int[] rank; // upper bound on tree height RankDSU(int n) { parent = new int[n]; rank = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; } /** Iterative find with path halving: point every other node at its grandparent. */ int find(int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; } boolean union(int a, int b) { int ra = find(a), rb = find(b); if (ra == rb) return false; if (rank[ra] < rank[rb]) parent[ra] = rb; else if (rank[ra] > rank[rb]) parent[rb] = ra; else { parent[rb] = ra; rank[ra]++; } // equal heights: the tree grows by one return true; }}rankis an upper bound on height; it grows only when two equal-rank trees merge.- Path halving (
parent[x] = parent[parent[x]]) is a one-pass, no-recursion form of compression with the same α(n) bound. - Size or rank — both give the same guarantees. Size is handy because problems often ask "how big is the biggest group?"
Use 1: counting connected components
class CountComponents { /** Number of connected components of an undirected graph with n nodes. */ static int count(int n, int[][] edges) { DSU dsu = new DSU(n); // n singletons = n components for (int[] e : edges) dsu.union(e[0], e[1]); // each successful union removes one return dsu.components(); }}- Every successful union merges two sets into one, so the number of sets drops by exactly one.
- This is the whole idea behind Number of Provinces, Number of Connected Components, and Make Network Connected.
Use 2: cycle detection in an undirected graph
class CycleDetection { /** Undirected graph: an edge whose ends are already connected closes a cycle. */ static boolean hasCycle(int n, int[][] edges) { DSU dsu = new DSU(n); for (int[] e : edges) { if (!dsu.union(e[0], e[1])) return true; } return false; }}- If u and v already have the same root, a path u ⇝ v exists; adding edge u–v closes a loop.
- Only for undirected graphs. Directed cycles need DFS colours or Kahn's algorithm (see Topological Sort).
Kruskal: sort the edges, let the DSU veto cycles
class KruskalMST { private int[] parent, size; private int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); // path compression return parent[x]; } private boolean union(int a, int b) { int ra = find(a), rb = find(b); if (ra == rb) return false; // already connected → this edge would close a cycle if (size[ra] < size[rb]) { int t = ra; ra = rb; rb = t; } parent[rb] = ra; size[ra] += size[rb]; return true; } /** edges[i] = {u, v, weight}. Returns the MST weight, or -1 if the graph is disconnected. */ long mst(int n, int[][] edges, List<int[]> chosen) { int[][] sorted = edges.clone(); Arrays.sort(sorted, Comparator.comparingInt(e -> e[2])); // cheapest first parent = new int[n]; size = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; } long total = 0; for (int[] e : sorted) { if (!union(e[0], e[1])) continue; // same component → skip total += e[2]; chosen.add(e); if (chosen.size() == n - 1) break; // a spanning tree has n − 1 edges } return chosen.size() == n - 1 ? total : -1; }}Arrays.sort(edges, Comparator.comparingInt(e -> e[2]))sortsint[]rows by weight. Copy first if the caller needs the original order.- Stop as soon as you have n − 1 edges — the rest can only be rejected.
- Works directly on an edge list, so it is natural when the input is a list of edges.
Prim: PriorityQueue vs a plain array
class DensePrim { /** w[u][v] = edge weight, or Integer.MAX_VALUE if there is no edge. O(V²), no heap. */ static long mst(int[][] w) { int n = w.length; int[] best = new int[n]; // cheapest known edge from the tree to each node boolean[] inTree = new boolean[n]; Arrays.fill(best, Integer.MAX_VALUE); best[0] = 0; long total = 0; for (int round = 0; round < n; round++) { int u = -1; for (int v = 0; v < n; v++) // linear scan replaces the heap if (!inTree[v] && (u == -1 || best[v] < best[u])) u = v; if (best[u] == Integer.MAX_VALUE) return -1; // unreachable → disconnected inTree[u] = true; total += best[u]; for (int v = 0; v < n; v++) if (!inTree[v] && w[u][v] < best[v]) best[v] = w[u][v]; } return total; }}class LazyPrim { /** adj.get(u) holds {v, w}. Returns the MST weight, or -1 if the graph is disconnected. */ static long mst(int n, List<List<int[]>> adj) { boolean[] inTree = new boolean[n]; // entries are {node, weight of the edge that would bring it in} PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1])); pq.offer(new int[] {0, 0}); // grow the tree from node 0 long total = 0; int added = 0; while (!pq.isEmpty() && added < n) { int[] top = pq.poll(); // cheapest edge leaving the tree int u = top[0]; if (inTree[u]) continue; // stale: u already joined via a cheaper edge inTree[u] = true; total += top[1]; added++; for (int[] e : adj.get(u)) { if (!inTree[e[0]]) pq.offer(new int[] {e[0], e[1]}); } } return added == n ? total : -1; }}- Java's
PriorityQueuehas no decrease-key, so we push duplicates and skip stale entries when polled ("lazy" Prim). new PriorityQueue<>(Comparator.comparingInt(a -> a[1]))ordersint[]entries by weight. Avoid(a, b) -> a[1] - b[1]when weights can be huge or negative — the subtraction can overflow.- On a complete graph (every pair is an edge) E ≈ V²/2, and the heap only adds a log factor. The array version is simpler and faster: O(V²).
| call | does | cost |
|---|---|---|
int[] parent = new int[n]; parent[i] = i | Make n singleton sets | O(n) |
Arrays.sort(edges, Comparator.comparingInt(e -> e[2])) | Sort edge rows {u, v, w} by weight (Kruskal) | O(E log E) |
new PriorityQueue<int[]>(Comparator.comparingInt(a -> a[1])) | Min-heap of {node, weight} (Prim) | offer/poll O(log n) |
pq.offer(x) / pq.poll() / pq.isEmpty() | Push, pop the smallest, test empty | O(log n) / O(log n) / O(1) |
map.putIfAbsent(x, x) | Create a singleton set for a new key (map DSU) | O(1) average |
map.computeIfAbsent(root, k -> new TreeSet<>()).add(x) | Group members by root, sorted (Accounts Merge) | O(1) avg + O(log k) |
Arrays.fill(best, Integer.MAX_VALUE) | Initialise "cheapest link" array (dense Prim) | O(n) |
Edges arrive one at a time; ask "connected?" between arrivals
→ Union-Find
Each union/find is ~O(1); re-running BFS after every edge would be O(V + E) each time.
One-off component count on a fixed graph
→ DFS/BFS or Union-Find
Both are linear. DFS is natural for grids/adjacency lists; DSU shines with edge lists.
Cycle detection, undirected
→ Union-Find
The first union that returns false is the cycle edge.
Cycle detection, directed
→ DFS colours / Kahn's algorithm
DSU ignores direction, so it would report false cycles (a→b, a→c, b→c).
MST from an edge list (sparse graph)
→ Kruskal
Sort + DSU is short and needs no adjacency list.
MST of a dense / complete graph (all pairs of points)
→ Dense Prim, O(V²)
Avoids building and sorting ~V²/2 edges.
Need to delete edges or split groups
→ Not DSU — re-run BFS/DFS (or offline tricks)
Union-Find can only merge; it cannot un-merge.
07 · recognise
Spot the pattern
"Groups", "connected", "same component", "friends of friends", "merge accounts"
Union-Find (or DFS) over the connection relation
Edges/connections are given as a list and arrive over time
Union-Find: union each edge, track count or detect the redundant one
"Which edge creates a cycle?" or "is this a valid tree?" in an undirected graph
The first union whose ends share a root; a tree also needs exactly n − 1 edges
Equalities and inequalities (a == b, b != c), or "is this assignment consistent?"
Union all equalities, then check every inequality spans two sets
"Connect all … with minimum total cost"
Minimum spanning tree — Kruskal (edge list) or Prim (dense)
"Minimum time/threshold until two points become connected" while something rises
Process cells/edges in sorted order with a DSU; stop when they join (or binary search + BFS)
"How many extra edges/operations to connect everything?"
Count components c; the answer is c − 1 (if enough edges exist)
08 · traps & tricks
Mistakes to avoid, tricks to use
traps
- Comparing
parent[a] == parent[b]instead offind(a) == find(b)— parents are not roots. - Writing
parent[a] = bin union instead ofparent[find(a)] = find(b)— you re-parent a node, not its whole set, and break other members. - Forgetting to initialise
parent[i] = i(a zero-filled array makes everyone point at 0). - Off-by-one with 1-indexed nodes (LeetCode 684): allocate
n + 1slots. - Using DSU for directed cycle detection — it ignores direction and reports false cycles.
- Kruskal without the "skip if same set" check, or Prim without the "skip if already in tree" check — you'll count edges twice or build cycles.
- Subtraction comparators
(a, b) -> a[2] - b[2]with large or negative weights: overflow flips the order. UseInteger.compare/Comparator.comparingInt. - Forgetting the disconnected case: if Kruskal ends with fewer than n − 1 edges (or Prim reaches fewer than n nodes) there is no spanning tree.
tricks
- Memorise one DSU template (arrays + size + recursive compression) and reuse it everywhere — interviewers expect it to be quick and bug-free.
- Say the complexity precisely: "amortized O(α(n)) per operation with both union by size and path compression — effectively constant."
- When the input is an edge list and the question is about connectivity, reach for DSU first; when it is a grid or adjacency list, DFS/BFS is often shorter. Mention both.
- Track extra data at the root (size, min, max, count of something) and merge it during union — many problems need just one extra array.
- For MST questions, explain the cut property in one sentence before coding; it shows you know why greedy is correct.
- Map 2D cells to 1D ids with
r * cols + cso anint[]DSU still works on grids. - Bas itna yaad rakho: DSU only merges. The moment a problem needs to split a group, it's the wrong tool.
09 · why & how
Questions you might be too shy to ask
Where are the groups actually stored? I only see one parent[] array.
Nowhere else — that's the trick. A group is just "everyone whose find gives the same root"; the DSU keeps no member list. When a problem wants the groups themselves (Accounts Merge), loop over every element once and bucket it by its root: map.computeIfAbsent(find(i), k -> new ArrayList<>()).add(i).
How does one line, parent[x] = find(parent[x]), flatten the whole path?
Take the chain 3 → 2 → 1 → 0, where 0 is the root. find(3) calls find(2), which calls find(1), which calls find(0) and gets 0 back. On the way back each call stores that answer: parent[1] = 0, then parent[2] = 0, then parent[3] = 0. Simple hai — the calls go up to find the root, and the return trip re-points every node, so the next find(3) is one hop.
What does "amortized O(α(n))" actually mean? Can one find be slow?
Yes, a single find can walk a longer path once. But it then flattens that path, so later finds are cheap. Averaged over any sequence of m operations, the cost is O(m·α(n)) total. α grows so slowly that it is at most 4 for any realistic n, so you can call it "near-constant".
Do I need both union by size and path compression?
Each alone gives O(log n) (worst case for size/rank, amortized for compression). Together they give O(α(n)). In interviews, write both — it is only two extra lines.
Path compression changes parent[]. Doesn't that break the sizes or ranks?
No. Compression only moves nodes within one tree, so the set and the root stay the same and the root's size stays correct. Rank becomes an upper bound on height instead of the exact height — which is all the analysis needs.
When should I use DFS/BFS instead of Union-Find?
For a fixed graph you traverse once (count islands, find a path), DFS/BFS is just as fast and needs no extra structure. Union-Find wins when edges arrive one by one and you need connectivity answers in between, or when the input is a plain list of pairs.
Accounts Merge: why union each email only with the account's first email, not every pair?
Being connected is transitive. If every email is joined to the first one, they all land in one set — k emails need only k − 1 unions (a star shape). Joining every pair would cost about k²/2 unions for the same result. And an email that appears in two accounts automatically bridges both sets.
Satisfiability of Equality Equations: why process every == before any !=?
A != check is only valid once the groups are final. Example: a!=c, a==b, b==c. Checked in that order, a!=c looks fine because a and c are still apart — then the two == merge them. Two passes fix it: first union all the ==, then test each != against the finished groups.
Make Network Connected: why are there always enough spare cables once cables ≥ n − 1?
A component with k computers needs only k − 1 cables; every extra cable inside it is spare. With c components the needed cables add up to n − c, so spares = cables − (n − c). If cables ≥ n − 1, spares ≥ (n − 1) − (n − c) = c − 1 — exactly the moves needed to join c pieces.
Why is it safe for Kruskal to grab the cheapest edge greedily?
Cut property: split the nodes into two sides, and the cheapest edge crossing the split belongs to some MST (if a tree skipped it, swapping it in for the tree's crossing edge would make the tree no heavier). Now split one endpoint's group from everything else. Every cheaper edge was already taken or joined two nodes in the same group, so Kruskal's next accepted edge is the cheapest one crossing that split.
Kruskal or Prim — which do I write in an interview?
Kruskal if you are given an edge list (sort + DSU is short and hard to get wrong). Prim if you have an adjacency list, or the O(V²) array version when every pair of nodes is connected (like LC 1584).
Does it matter which node Prim starts from? What if the graph is disconnected?
Any start node works: the tree it grows always has the minimum total weight (with tied weights the chosen edges may differ, the total never does). But Prim only grows inside the start node's component. If it reaches fewer than n nodes, the graph is disconnected and there is no spanning tree — Kruskal shows the same thing by ending with fewer than n − 1 edges.
Is the minimum spanning tree the same as shortest paths?
No. An MST minimises the total weight of the tree; the path between two nodes inside the MST can be much longer than the shortest path (Dijkstra). Different goals, different algorithms.
Do negative weights or equal weights break MST algorithms?
No. Kruskal and Prim only compare weights, so negatives are fine. Equal weights just mean the MST may not be unique — but every MST has the same total weight. (With all weights distinct, the MST is unique.)
10 · practice
Interview problems
Every problem has brute → better → optimal solutions in Java, with complexity and a dry run.
- #547
- #323
- #684
- #261
- #721
- #1584
- #990Satisfiability of Equality EquationsMediumAmazon
- #1319Number of Operations to Make Network ConnectedMediumAmazon
- #1489
Also practise this here
Each problem has one home page in the course; these use this chapter's idea too. The note says what to look for.
- #128Longest Consecutive SequenceMediumin Hashing
A map-based DSU over values: union x with x + 1 and track set sizes (its Union-Find approach).
- #778Swim in Rising WaterHardin Shortest Paths
Flood cells in elevation order and union with flooded neighbours; the first time the corners share a root is the answer.
11 · revise
The whole chapter on one screen
9 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.
- what it is
Union-Find:
parent[]forest, root = group name.findwalks up;unionlinks two roots. MST: the cheapest set of n − 1 edges connecting everything. - use it when
Pairs or edges arrive and you ask "same group?", count groups, spot the edge that closes a cycle — or "connect all at minimum cost".
- template
find:if (parent[x] != x) parent[x] = find(parent[x]);·union: roots equal → false; else smaller size under bigger,count--. - cost
Both tricks: O(α(n)) amortized per op — effectively constant. Kruskal O(E log E). Lazy Prim O(E log E). Dense Prim O(V²).
- trap
parent[a] == parent[b]is not "same set" — comparefind(a) == find(b). And link roots:parent[find(a)] = find(b). - trap
DSU ignores direction: never use it for directed cycles (use DFS colours or Kahn's). It also can't split a group.
- pro tip
Valid tree = exactly n − 1 edges + no union fails. Make network connected = components − 1, if cables ≥ n − 1.
- pro tip
Every pair of points is an edge? Use dense Prim with a
best[]array — no 5-lakh-edge list to build and sort. - trap
Kruskal that ends with fewer than n − 1 edges means disconnected — there is no spanning tree. Check it.
12 · recall
Flip, remember, repeat
What two operations does Union-Find support, and what does a root mean?
Question 1: What two operations does Union-Find support, and what does a root mean?
13 · check yourself
Quick quiz
`parent = [0, 0, 1, 3, 3]`. What does `find(2)` return, and what is `parent[2]` after path compression?
With both union by size and path compression, what is the cost of each operation?
Which question can union-find NOT answer efficiently?
Kruskal is processing edge 2–5 (weight 4), and `find(2) == find(5)`. What happens?
A connected graph has 7 nodes. How many edges does any spanning tree have?
You must connect 1000 points where every pair can be linked (cost = distance). Which MST approach is best?
Why can't you use union-find to detect a cycle in a *directed* graph?
n = 4 and edges = [[0,1],[1,2],[2,0]]. The count is n − 1 = 3. Is it a valid tree?
14 · learn more
A second teacher, if you want one
- ▶ videoG-46. Disjoint Set | Union by Rank | Union by Size | Path Compression ↗Striver (take U forward) · EnglishUnion by rank, union by size and path compression in one lesson, ending in a small DSU class you can reuse as a template.
- ▶ playlistUnion Find [Disjoint Set] Playlist ↗William Fiset · EnglishFive short videos: the idea, Kruskal's MST with union-find, the union and find operations, path compression, and the code.
- ▶ video3.5 Prims and Kruskals Algorithms - Greedy Method ↗Abdul Bari · EnglishPrim's and Kruskal's on the same graph: two greedy routes to one minimum spanning tree, shown side by side.