An interactive visualization of the Union-Find (Disjoint Set) approach to counting connected components in an undirected graph. Watch how path compression and union by rank efficiently merge components.
Union-Find LeetCode 323 ↗ Mediumn nodes labeled from 0 to n - 1.
You are given an integer n and a list of edges where
edges[i] = [a_i, b_i] indicates that there is an undirected edge between
nodes a_i and b_i in the graph.fun countComponents(n: Int, edges: Array<IntArray>): Int { val parent = IntArray(n) { it } val rank = IntArray(n) fun find(x: Int): Int { var i = x while (parent[i] != i) { parent[i] = parent[parent[i]]; i = parent[i] } return i } fun union(x: Int, y: Int): Boolean { var px = find(x); var py = find(y) if (px == py) return false if (rank[px] < rank[py]) { val t = px; px = py; py = t } parent[py] = px if (rank[px] == rank[py]) rank[px]++ return true } var components = n for ((u, v) in edges) { if (union(u, v)) components-- } return components}
Instead of running BFS/DFS from every node (which requires building an adjacency list),
we use Union-Find (Disjoint Set Union) to process edges one at a time.
Each edge [u, v] merges the components containing u and v.
We start with n components (each node is its own component) and decrement every time
a union actually merges two distinct components.
parent[i] = i and rank[i] = 0 for each node. Set components = n.[u, v], call union(u, v).px and py. If they are the same, the nodes are already connected (return False). Otherwise, attach the smaller-rank tree under the larger-rank root (union by rank) and return True.components by 1.components holds the answer.
With path compression and union by rank, each find and union operation
runs in amortized O(α(n)) time, where α is the inverse Ackermann function —
effectively constant for all practical input sizes. We process each of the E edges once,
so total time is O(E · α(n)). Space is O(n) for the parent and rank arrays.