Union-Find
Loading…
One-liner: keep track of which items belong to the same group, merge two groups in almost constant time, and ask "are these two together?" without walking the whole graph.
Picture a party where people keep discovering they are friends. You do not want a list of every friendship. You only want a group leader for each circle of friends. To ask "are Ana and Bo in the same circle?", you find each one's leader and compare. When two circles meet, one leader simply starts reporting to the other. That is Union-Find: a leader per group, and merging is one reassignment.
Reach for Union-Find when you see:
Five nodes 0..4 and edges [0,1], [1,2], [3,4]:
0 and 1: they become one group. Four groups.1 and 2: 1 is already with 0, so 2 joins them. Three groups.3 and 4: two groups left, {0,1,2} and {3,4}.[0,2] arrived next, Find(0) and Find(2) give the same leader. Union returns false: this edge would close a cycle.A parent slice plus two operations. The comments mark the four ideas that make it fast.
// UF tracks which items belong to the same group. Find answers "which group?", Union merges two groups.
type UF struct {
parent []int
size []int
groups int
}
func NewUF(n int) *UF {
u := &UF{parent: make([]int, n), size: make([]int, n), groups: n}
for i := range u.parent {
u.parent[i] = i // 1. everyone starts as their own group's root
u.size[i] = 1
}
return u
}
// Find returns the root of x's group, flattening the path as it goes.
func (u *UF) Find(x int) int {
for u.parent[x] != x {
u.parent[x] = u.parent[u.parent[x]] // 2. path compression: point at the grandparent
x = u.parent[x]
}
return x
}
// Union merges the groups of a and b. It reports false if they were already together.
func (u *UF) Union(a, b int) bool {
ra, rb := u.Find(a), u.Find(b)
if ra == rb {
return false // 3. already connected: this edge would close a cycle
}
if u.size[ra] < u.size[rb] {
ra, rb = rb, ra
}
u.parent[rb] = ra // 4. attach the smaller tree under the larger
u.size[ra] += u.size[rb]
u.groups--
return true
}Why each part exists:
u.parent[i] = iEvery item starts as its own leader. A leader is any item whose parent is itself.
u.parent[x] = u.parent[u.parent[x]]Path compression. While walking up to the leader, point each item at its grandparent. The next lookup is shorter, so chains flatten over time instead of growing into a long list.
if ra == rb { return false }Two items with the same leader are already connected. Returning false here is how you detect a cycle, or count a redundant edge.
u.parent[rb] = ra // smaller under largerUnion by size. Always hang the smaller tree under the bigger one, so trees stay shallow. Together with path compression, each operation is close to constant time (inverse Ackermann).
Number of connected components — start with n groups and subtract one for every successful union:
// CountComponents (LeetCode 323): number of connected components in an undirected graph of n nodes.
func CountComponents(n int, edges [][]int) int {
u := NewUF(n)
for _, e := range edges {
u.Union(e[0], e[1])
}
return u.groups
}Redundant connection — the first edge whose two ends are already connected is the one that closes the loop:
// FindRedundantConnection (LeetCode 684): the edge that closes a cycle in a graph that was a tree plus one extra edge.
// The input has n nodes and n edges, so nodes 1..n fit in n+1 slots. If both ends are already connected, this edge makes the cycle.
func FindRedundantConnection(edges [][]int) []int {
u := NewUF(len(edges) + 1) // nodes are labelled 1..n
for _, e := range edges {
if !u.Union(e[0], e[1]) {
return e
}
}
return nil
}Number of provinces — the same idea over an adjacency matrix:
// FindCircleNum (LeetCode 547): number of friend groups in an adjacency matrix.
func FindCircleNum(connected [][]int) int {
n := len(connected)
u := NewUF(n)
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
if connected[i][j] == 1 {
u.Union(i, j)
}
}
}
return u.groups
}Valid tree — a tree has exactly n - 1 edges and no cycle:
// ValidTree (LeetCode 261): n nodes form a tree if there are exactly n-1 edges and no edge closes a cycle.
func ValidTree(n int, edges [][]int) bool {
if len(edges) != n-1 {
return false
}
u := NewUF(n)
for _, e := range edges {
if !u.Union(e[0], e[1]) {
return false
}
}
return true
}For graph searches that need paths, distances or an ordering, see Graphs.
| Approach | Time | Space |
|---|---|---|
| Re-run DFS after every new edge | O(E · (V + E)) | O(V + E) |
| Union-Find (path compression + union by size) | O(E · α(V)) ≈ O(E) | O(V) |
α is the inverse Ackermann function. It is below 5 for any input that fits in memory, so treat each operation as constant.
Ordered Easy → Hard. For each, decide: do connections arrive over time, and do I need the path or only the yes/no?
Unlabeled problems — pick the pattern, then read why.
You are given the number of computers and a list of cables that connect pairs of them. How many separate networks are there?
Which pattern?