Expert: advanced patterns and interviews · Tries, union-find and bit manipulation · topic 48 of 61 · 35 min
Union-find (disjoint set union)
Explanation
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.
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 FalseTwo 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.
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 rootUnion 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
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.
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()))Practice (0/4 done)
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.
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.
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.
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Minimum spanning treesTries, union-find and bit manipulation · same module
- Bits: AND, OR, XOR and shiftsTries, union-find and bit manipulation · same module
- UNION, INTERSECT and EXCEPTSQL and Databases · Subqueries, CTEs and set operations
- Unions, enums and memory layoutC Programming (from zero to systems) · Structs, unions, enums and files
- Object types, interfaces and unionsJavaScript Programming (language in depth) · TypeScript basics
- Unions and narrowingFrontend Web Development · TypeScript basics
Questions people ask about this
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.
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.
What is the size of a union?Related
Answer: At least the size of its largest member
Members overlap (plus possible padding).
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.
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
- 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.
- Module 10 exam: Tries, union-find and bits · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Dynamic programming. 61 topics left in the course before the final exam.