Skip to content

Expert: advanced patterns and interviews · Tries, union-find and bit manipulation · topic 48 of 61 · 35 min

Union-find (disjoint set union)

Explanation

Union-find (also called disjoint set union, DSU) keeps items in groups that can only merge. Each item points to a parent, and the root of the chain names the group. find follows parents to the root, and union joins two roots. With path compression and union by size, each operation is almost O(1) amortised, which makes DSU ideal for "are these connected?" questions as edges arrive.

The question: are these two in the same group?

Some problems keep asking "are x and y connected?" while new connections keep arriving: friends joining friend circles, computers being cabled into networks, or bank accounts found to belong to the same customer. Groups only ever MERGE. Running a BFS for every question would cost O(V + E) each time. Union-find answers each question in nearly constant time.

A forest of parent pointers

Give every item a parent. At the start, every item is its own parent, so each item is a group of one. The item at the top of a chain, whose parent is itself, is the root, and it acts as the name (the representative) of the whole group. Two items are in the same group exactly when they have the same root.

  • find(x): follow parent pointers from x until you reach the root, and return it.
  • union(a, b): find the root of a and the root of b. If they differ, make one root the parent of the other. Now both groups share one root.
python
parent = list(range(5))          # [0, 1, 2, 3, 4]: everyone alone

def find(x):
    while parent[x] != x:
        x = parent[x]
    return x

def union(a, b):
    ra, rb = find(a), find(b)
    if ra != rb:
        parent[rb] = ra

union(0, 1)
union(3, 4)
union(1, 4)
print(parent)                                   # Output: [0, 0, 2, 0, 3]
print(find(4) == find(0), find(2) == find(0))   # Output: True False
The simple version. Notice that union joins the ROOTS, not a and b themselves.

Two speed-ups

The simple version can build long chains: union in a bad order and find walks O(n) steps. Two small changes fix this.

Path compression: during find, after you reach the root, point every node you passed straight at the root. The next find on any of them takes one step.

python
def find(x):
    root = x
    while parent[root] != root:
        root = parent[root]
    while x != root:          # second pass: compress the path
        nxt = parent[x]
        parent[x] = root
        x = nxt
    return root
The same answer as before, but the chain is flattened for next time.

Union by size (or by rank): keep the size of each group at its root, and always hang the SMALLER tree under the larger one. A node then moves one level deeper only when its group at least doubles, so no tree gets taller than about log₂ n.

With both changes, any sequence of m operations on n items takes O(m × α(n)) time, where α is the inverse Ackermann function. It grows so slowly that it is at most 4 for any n you could ever store, so in practice each operation is constant time (amortised: averaged over many operations). The parent and size lists take O(n) space.

Counting groups and classic uses

Keep a count that starts at n. Every union that really joins two different roots lowers it by 1. At the end, count is the number of groups.

  • Number of provinces: cities joined directly or through others; union every direct link and read the count.
  • Redundant connection: edges of a tree arrive plus one extra. The first edge whose two ends ALREADY have the same root closes a cycle, so it is the extra one.
  • Earliest moment everyone is connected: sort events by time and union until the count reaches 1.
  • Kruskal's minimum spanning tree (next topic) uses DSU to skip edges that would form a cycle.

DSU cannot easily split a group or delete an edge. If the graph is fixed and you need paths or distances, BFS or DFS is better. If edges arrive one by one and you only merge and ask "same group?", DSU shines.

Common mistakes

  • Joining the items instead of their roots (parent[a] = b). Fix: always union find(a) and find(b).
  • Comparing parent[a] == parent[b] to test "same group". Parents can differ while roots are equal. Fix: compare find(a) == find(b).
  • Lowering the group count even when the two items were already together. Fix: only when the roots differ.
  • Labels from 1 to n with a list of size n. Fix: allocate n + 1, or subtract 1.
  • A recursive find on a long chain before any compression, which can hit the recursion limit. Fix: an iterative find, or union by size so chains stay short.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Union-find (disjoint set union) · 1/8

Union-find

This lesson is about union-find, also called disjoint set union: a structure for groups that only ever merge.

Prefer a video by a teacher? Find free videos on “union find disjoint set path compression union by rank explained” on YouTube ↗

In real life: Merging duplicate customer accounts

Banks and online shops sometimes find that one person has opened several accounts. If two accounts share an email address or a phone number, they probably belong to the same customer. Union-find merges them: each time an identifier has been seen before, union the current account with the account that first used it. Each final group is one customer. Real systems add careful checks before merging, but this grouping step is the core. The identifiers below are made up.

python
accounts = {
    "acc1": ["asha@example.com", "phone-0001"],
    "acc2": ["asha.k@example.com"],
    "acc3": ["asha@example.com", "asha.k@example.com"],
    "acc4": ["ravi@example.com"],
}
parent = {a: a for a in accounts}

def find(x):
    while parent[x] != x:
        parent[x] = parent[parent[x]]
        x = parent[x]
    return x

first_owner = {}
for acc, ids in accounts.items():
    for ident in ids:
        if ident in first_owner:
            parent[find(acc)] = find(first_owner[ident])
        else:
            first_owner[ident] = acc
groups = {}
for acc in accounts:
    groups.setdefault(find(acc), []).append(acc)
print(list(groups.values()))
Shows [['acc1', 'acc2', 'acc3'], ['acc4']]: acc3 links acc1 and acc2, so they are one customer.

Practice (0/4 done)

Task 1 · Code · Python

Write a class DSU for items 0 to n − 1. DSU(n) starts with every item in its own group. find(x) returns the root of x's group. union(a, b) joins the two groups and returns True, or returns False if a and b were already in the same group. count() returns the current number of groups. Use path compression and union by size.

Task 2 · Code · Python

There are n cities. is_connected is an n × n matrix where is_connected[i][j] == 1 means cities i and j are directly connected (the matrix is symmetric and is_connected[i][i] == 1). A province is a group of cities connected directly or through other cities. Write count_provinces(is_connected) that returns the number of provinces, using union-find. An empty matrix has 0 provinces.

Task 3 · Code · Python

A graph started as a tree with n nodes labelled 1 to n, and then ONE extra edge was added, so edges has n pairs [u, v]. Write find_redundant_connection(edges) that returns the edge that can be removed so the rest is a tree again. If several edges would work, return the one that appears LAST in edges. Return it as a list [u, v], exactly as given. Hint: with union-find, it is the first edge whose two ends already have the same root.

Task 4 · Code · Python

People are labelled 0 to n − 1. logs is a list of (time, a, b): at that time, a and b became friends. Friendship is shared through friends, so groups merge. Write earliest_all_connected(logs, n) that returns the earliest time at which all n people are in one group, or -1 if that never happens. logs may not be sorted. If n is 0 or 1, everyone is already connected: return 0.

Quick quiz

Question 1

1.What does find(x) return?

Question 2

2.What does path compression do?

Question 3

3.Why hang the smaller tree under the larger one in union?

Question 4

4.While adding the edges of a graph to a DSU, you reach an edge whose two ends already have the same root. What does that edge do?

Question 5

5.Which operation does a DSU NOT support well?

Question 6

6.With path compression and union by size, what is the amortised cost per operation?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does find(x) return?

Answer: The root (representative) of x's group

Two items are in the same group exactly when find returns the same root for both.

What does path compression do?

Answer: Points every node on the find path straight at the root

Later finds on those nodes take a single step.

Why hang the smaller tree under the larger one in union?

Answer: To keep trees shallow, so finds stay short

A node gets deeper only when its group at least doubles, so depth stays around log n.

While adding the edges of a graph to a DSU, you reach an edge whose two ends already have the same root. What does that edge do?

Answer: It closes a cycle

The ends were already connected, so the new edge makes a second path between them.

Which operation does a DSU NOT support well?

Answer: Splitting a group, or removing an edge

DSU only merges; undoing a merge needs a different structure.

With path compression and union by size, what is the amortised cost per operation?

Answer: O(α(n)), practically constant

α (inverse Ackermann) is at most 4 for any realistic n.

SELECT name, city FROM customers UNION SELECT city, name FROM departments. What happens?Related

Answer: It runs, but names and cities are mixed in the same column, because matching is by position

Set operations line columns up by position; keep the same order on both sides.

From UNION, INTERSECT and EXCEPT

What is the time of Kruskal's algorithm with union-find?Related

Answer: O(E log E)

Sorting the edges dominates; union-find adds almost O(1) per edge.

From Minimum spanning trees

What is the size of a union?Related

Answer: At least the size of its largest member

Members overlap (plus possible padding).

From Unions, enums and memory layout

What makes a union "discriminated"?Related

Answer: Every member has a common field with a different literal value

The shared literal field lets TypeScript narrow precisely.

From Object types, interfaces and unions

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Tries, union-find and bit manipulation” before its exam: Minimum spanning trees, Bits: AND, OR, XOR and shifts, Bit tricks in interviews.
  2. Module 10 exam: Tries, union-find and bits · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Dynamic programming. 61 topics left in the course before the final exam.

← Previous: Tries (prefix trees)