28

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.

intermediate~70 min4 simulations9 problems

▶ guided lesson⚡ 1-minute summary↻ 14 recall cards

before this: Graphs · Recursion · Sorting · Heaps

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 connectedComponents offers 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; recursive find that re-points nodes at the root; union returns false when the roots already match. For string keys, a HashMap<String, String> parent map.

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 has parent[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 does count--. 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.

sim

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.

loading simulation…
sim

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).

loading simulation…
sim

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.

loading simulation…
sim

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.

loading simulation…

05 · cost

Time & space, explained

Union-Find: what each trick buys you (n elements)
find / unionSpacewhy
Naive quick-union (no tricks)O(n) worstO(n)Unions can build a chain like 0←1←2←…; find from the bottom walks the whole chain.
Union by size or rank onlyO(log n) worstO(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 onlyO(log n) amortizedO(n)Long paths are flattened the first time they are walked, so expensive finds pay for cheap future ones (Tarjan & van Leeuwen).
Both tricksO(α(n)) amortizedO(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, averageO(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.)
Minimum spanning tree algorithms (V nodes, E edges)
TimeExtra spacewhy
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 treeExponential—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

javaint[] parent + sizejava.util.*
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; }}
  • 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⁹). putIfAbsent creates singleton sets lazily.
  • Return a boolean from union: false means "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

javano balancing, no compressionjava.util.*
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

javarank + path halvingjava.util.*
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;    }}
  • rank is 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

javacount = n, minus one per mergejava.util.*
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

javaedge inside one set = cyclejava.util.*
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

javasort + DSUjava.util.*
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])) sorts int[] 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

javadense Prim, O(V²)java.util.*
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;    }}
  • Java's PriorityQueue has no decrease-key, so we push duplicates and skip stale entries when polled ("lazy" Prim).
  • new PriorityQueue<>(Comparator.comparingInt(a -> a[1])) orders int[] 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²).
Java API you will actually use
calldoescost
int[] parent = new int[n]; parent[i] = iMake n singleton setsO(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 emptyO(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 of find(a) == find(b) — parents are not roots.
  • Writing parent[a] = b in union instead of parent[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 + 1 slots.
  • 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. Use Integer.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 + c so an int[] 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.

  1. #547
    Number of Provincesmust-do
    MediumBloombergAmazonGoogleMicrosoftMeta
  2. #323
    Number of Connected Components in an Undirected GraphLeetCode Premium
    MediumLinkedInAmazonGoogle
  3. #684
    Redundant Connectionmust-do
    MediumGoogleAmazonMicrosoft
  4. #261
    Graph Valid Treemust-doLeetCode Premium
    MediumLinkedInGoogleAmazon
  5. #721
    Accounts Mergemust-do
    MediumMetaAmazonMicrosoftGoogle
  6. #1584
    Min Cost to Connect All Pointsmust-do
    MediumAmazonGoogleMicrosoft
  7. #990
  8. #1319
  9. #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.

  1. #128
    Longest Consecutive Sequence
    Mediumin Hashing

    A map-based DSU over values: union x with x + 1 and track set sizes (its Union-Find approach).

  2. #778
    Swim in Rising Water
    Hardin 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.

  1. what it is

    Union-Find: parent[] forest, root = group name. find walks up; union links two roots. MST: the cheapest set of n − 1 edges connecting everything.

  2. 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".

  3. template

    find: if (parent[x] != x) parent[x] = find(parent[x]); · union: roots equal → false; else smaller size under bigger, count--.

  4. 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²).

  5. trap

    parent[a] == parent[b] is not "same set" — compare find(a) == find(b). And link roots: parent[find(a)] = find(b).

  6. trap

    DSU ignores direction: never use it for directed cycles (use DFS colours or Kahn's). It also can't split a group.

  7. pro tip

    Valid tree = exactly n − 1 edges + no union fails. Make network connected = components − 1, if cables ≥ n − 1.

  8. 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.

  9. trap

    Kruskal that ends with fewer than n − 1 edges means disconnected — there is no spanning tree. Check it.

12 · recall

Flip, remember, repeat

card 1 / 14
recall

What two operations does Union-Find support, and what does a root mean?

tap or Space to flip ↻

Question 1: What two operations does Union-Find support, and what does a root mean?

13 · check yourself

Quick quiz

  1. `parent = [0, 0, 1, 3, 3]`. What does `find(2)` return, and what is `parent[2]` after path compression?

  2. With both union by size and path compression, what is the cost of each operation?

  3. Which question can union-find NOT answer efficiently?

  4. Kruskal is processing edge 2–5 (weight 4), and `find(2) == find(5)`. What happens?

  5. A connected graph has 7 nodes. How many edges does any spanning tree have?

  6. You must connect 1000 points where every pair can be linked (cost = distance). Which MST approach is best?

  7. Why can't you use union-find to detect a cycle in a *directed* graph?

  8. n = 4 and edges = [[0,1],[1,2],[2,0]]. The count is n − 1 = 3. Is it a valid tree?