Disjoint Set Union (union-find)
- Module
- M5.10
- Lesson
- 1 of 1
- Reading time
- 4 min
In this lesson
- Implement find with path halving to track connectivity across components.
- Apply union by size to keep the tree shallow and fast.
- Use the "next free slot" DSU trick to solve resource allocation problems.
- Recognize when DSU contracts components for efficient solutions.
Some problems ask you to track which items belong together. Friends in a social network form friend groups. Cities connected by roads form regions. Rock samples from the same deposit form clusters. You start with each item alone, then learn that pairs belong together, and need to answer whether two items are in the same group.
A disjoint set unionA data structure that tracks which elements belong to the same group, supporting fast union and connectivity queries.In the glossary data structure, also called union-findAnother name for disjoint set union; commonly used when describing the find and union operations.In the glossary, tracks these groups efficiently. You can ask "are A and B in the same group?" and merge two groups when you learn they are connected. Both operations run in nearly constant time.
Find and union with trees
The simplest idea is to use a parent array. Each element points to its parent. An element with no parent is a root. All elements in the same tree have the same root, so they are in the same group.
To check if A and B are in the same group, follow parents up from A to its root and from B to its root. If the roots match, they are in the same group. The operation is called find.
To merge the group containing A with the group containing B, find their roots. Then make one root a child of the other. The operation is called union.
Here is a simple example. Start with five independent elements.
def find(parent, x): """Follow parent pointers to the root.""" while parent[x] != x: x = parent[x] return x
def union(parent, a, b): """Merge the groups containing a and b.""" root_a = find(parent, a) root_b = find(parent, b) if root_a != root_b: parent[root_a] = root_b
# Start with 5 independent elementsparent = [0, 1, 2, 3, 4]
# Union 0 and 1union(parent, 0, 1)print("After union(0, 1):", parent)
# Union 2 and 3union(parent, 2, 3)print("After union(2, 3):", parent)
# Check connectivityprint("find(0):", find(parent, 0))print("find(1):", find(parent, 1))print("find(2):", find(parent, 2))print("find(3):", find(parent, 3))Output
After union(0, 1): [1, 1, 2, 3, 4]
After union(2, 3): [1, 1, 3, 3, 4]
find(0): 1
find(1): 1
find(2): 3
find(3): 3This code creates a parent array where each element is its own parent initially. Both find and union work.
Path halving keeps trees shallow
A problem with trees is that they can become very deep. If you merge by always making the same root a child of another, you build a chain. Then find on the deepest element takes linear time. Doing this a million times is far too slow.
The fix is to flatten trees as you traverse them. When you follow a parent pointer from node u to its parent parent[u], also point u to its grandparent parent[parent[u]]. This is called path halvingAn optimization in the find operation where each node points to its grandparent instead of its parent, flattening the tree over time.In the glossary. Over many finds, the trees stay shallow.
def find(parent, x): """Find root with path halving.""" while parent[x] != x: parent[x] = parent[parent[x]] # Skip one level x = parent[x] return x
def union(parent, a, b): """Merge the groups containing a and b.""" root_a = find(parent, a) root_b = find(parent, b) if root_a != root_b: parent[root_a] = root_b
# Build a chain by naive unions: 0 <- 1 <- 2 <- 3 <- 4parent = list(range(5))for i in range(4): union(parent, i, i + 1)
print("Parent array after naive unions:", parent)
# First find on 0: takes 4 steps without halvingprint("find(0) with path halving:", find(parent, 0))
# After halving, the path is shorterprint("Parent array after find:", parent)Output
Parent array after naive unions: [1, 2, 3, 4, 4]
find(0) with path halving: 4
Parent array after find: [2, 2, 4, 4, 4]The find function now climbs to the root in fewer steps. Each time you call find on an element, the path gets shorter.
Union by size prevents imbalance
Another way to keep trees shallow is to always attach the smaller tree under the larger tree. When you merge two groups, find their roots and attach the root of the smaller tree under the root of the larger tree. This is called union by sizeAn optimization where the smaller tree is always attached under the larger tree during union, keeping the forest shallow.In the glossary.
Combining path halving with union by size makes each operation run in nearly constant time. After any sequence of operations, a find or union takes time proportional to the logarithm of the number of elements, or often much faster in practice.
def find(parent, x): """Find root with path halving.""" while parent[x] != x: parent[x] = parent[parent[x]] x = parent[x] return x
def union(parent, size, a, b): """Merge groups containing a and b; attach smaller tree under larger.""" root_a = find(parent, a) root_b = find(parent, b) if root_a == root_b: return # Attach smaller tree under larger if size[root_a] < size[root_b]: parent[root_a] = root_b size[root_b] += size[root_a] else: parent[root_b] = root_a size[root_a] += size[root_b]
# Start with 5 elementsn = 5parent = list(range(n))size = [1] * n
# Union 0 and 1union(parent, size, 0, 1)print("After union(0, 1):", parent)
# Union 2 and 3union(parent, size, 2, 3)print("After union(2, 3):", parent)
# Union the two groupsunion(parent, size, 0, 2)print("After union(0, 2):", parent)print("Sizes:", size)Output
After union(0, 1): [0, 0, 2, 3, 4]
After union(2, 3): [0, 0, 2, 2, 4]
After union(0, 2): [0, 0, 0, 2, 4]
Sizes: [4, 1, 2, 1, 1]The size array tracks how many elements are in each tree. When you merge two roots, you attach the smaller one under the larger and update the size.
Connectivity in a network
Imagine a system where computers are initially isolated. You learn about five new network cables, each connecting two computers. Your job is to report when two computers are connected, even indirectly through other machines.
This is a natural use case for union-find. You start with each computer as its own group. Then you process each cable by calling union on its two endpoints. Afterward, you can query whether two computers are connected by calling find and comparing roots.
import sys
def main() -> None: input_data = sys.stdin.read().split() if not input_data: return
pos = 0 n = int(input_data[pos]); pos += 1 m = int(input_data[pos]); pos += 1
parent = list(range(n)) size = [1] * n
def find(x: int) -> int: while parent[x] != x: parent[x] = parent[parent[x]] x = parent[x] return x
def union(a: int, b: int) -> None: root_a = find(a) root_b = find(b) if root_a == root_b: return if size[root_a] < size[root_b]: parent[root_a] = root_b size[root_b] += size[root_a] else: parent[root_b] = root_a size[root_a] += size[root_b]
for _ in range(m): u = int(input_data[pos]); pos += 1 v = int(input_data[pos]); pos += 1 union(u, v)
q = int(input_data[pos]); pos += 1 out_lines = [] for _ in range(q): a = int(input_data[pos]); pos += 1 b = int(input_data[pos]); pos += 1 out_lines.append("yes" if find(a) == find(b) else "no")
print("\n".join(out_lines))
if __name__ == "__main__": main()Input
5 3
0 1
1 2
3 4
3
0 2
0 3
1 3Output
yes
no
noThe input gives the number of computers and cables, then one line per cable with two computer ids, then the number of queries and one line per query with two computer ids. The program prints yes or no for each query. Union-find answers each query instantly once all the cables have been processed.
The next free slot trick
A clever optimization applies DSU in a different way. Suppose you have a list of parking slots and many parked cars. When a car leaves, its slot becomes free. When a new car arrives, it wants to park in the lowest-numbered free slot. A brute-force search takes linear time per arrival. With DSU, you can answer in nearly constant time.
The trick is to use DSU on the slots themselves. When a car parks in slot s, you union s with s + 1. This creates a tree rooted at the first free slot after s. When the next car asks for the lowest free slot, you start at slot 1 and call find(1). If slot 1 is full, find(1) jumps past it to the root of its tree, which points to the first free slot.
def find(parent, x): """Find root with path halving; returns first free slot at or after x.""" if parent[x] == x: return x parent[x] = find(parent, x + 1) return parent[x]
def main() -> None: n = 5 parent = list(range(n + 1))
results = [] for _ in range(3): # Find next free slot slot = find(parent, 1) results.append(str(slot)) # Mark it occupied by pointing it to the next slot if slot < n: parent[slot] = slot + 1
print("\n".join(results))
if __name__ == "__main__": main()Output
1
2
3Here you have parking slots 1 through N. Each car arrives and parks in the lowest free slot, then leaves after one turn. The output is which slot each car used. Union-find skips over full slots in one operation instead of searching sequentially.
Common mistakes
A common mistake is using recursion for find. If the tree becomes deep before path halving takes effect, a recursive find hits Python's recursion limit. Always use a loop.
Another error is forgetting to update the size when you union. If you attach one root under another without updating the size of the parent, future unions may attach the larger tree under the smaller one, defeating the purpose.
Avoid building an adjacency list and using BFS just to check connectivity. Union-find handles that exact question with less code and less time.
Practice
Try these on the judge. Each link opens the problem on DMOJ.
- 2015 S3Gates (opens on DMOJ in a new tab) DMOJ
Find the next available gate on a linear arrangement using DSU next-free optimization.
Why DMOJ: A direct application of the next-free-slot trick from this module.
- 2017 S4Minimum Cost Flow (opens on DMOJ in a new tab) DMOJ
Merge components with DSU while breaking ties between equally cheap connections.
Why DMOJ: The full problem builds a minimum spanning tree, a later module's topic, but its DSU merging step is good practice here.