Connected Components
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given n vertices labelled 0..n-1 and a list of undirected edges {u, v}. A connected component is a maximal group of vertices in which every vertex can reach every other one along edges. Return the number of connected components.
This is the idea the A2Z lecture on connected components is built around: a graph does not have to be in one piece, so any algorithm that explores it from a single start vertex may miss parts of it. The Blind 75 version (LeetCode 323) asks for exactly this count.
Constraints: 1 ≤ n ≤ 2000, at most 5000 edges. LeetCode promises no repeated edges, but the solutions below also handle repeated edges and self-loops, since other judges are less strict.
Example 1
n = 5, edges = [[0,1], [1,2], [3,4]]
answer = 2 // {0, 1, 2} and {3, 4}
Example 2
n = 6, edges = [[0,1], [1,2], [2,0], [3,4]]
answer = 3 // the triangle {0, 1, 2}, the pair {3, 4}, and vertex 5 on its own
Vertex 5 in example 2 has no edges at all, but it is still a component. Isolated vertices are the easiest thing to forget.
Approach 1: Brute force, label propagation
Intuition. Give every vertex a label, initially its own number. An edge says its two ends belong together, so whenever an edge joins two different labels, overwrite both with the smaller one. Labels can only flow along edges, so they never cross between components; and as long as two neighbours disagree something still changes. When a full sweep over the edges changes nothing, every component has collapsed to a single label: the smallest vertex number in it. Counting components is then counting the vertices whose label is still their own number.
Algorithm.
- Set
label[i] = ifor every vertex. - Repeat: for every edge
{u, v}, set both labels tomin(label[u], label[v]); remember whether anything changed. - Stop after a sweep with no change.
- Return how many
isatisfylabel[i] == i.
static int bruteForce(int n, int[][] edges) {
int[] label = new int[n];
for (int i = 0; i < n; i++) label[i] = i;
boolean changed = true;
while (changed) {
changed = false;
for (int[] e : edges) {
int low = Math.min(label[e[0]], label[e[1]]);
if (label[e[0]] != low || label[e[1]] != low) {
label[e[0]] = low;
label[e[1]] = low;
changed = true;
}
}
}
int components = 0;
for (int i = 0; i < n; i++) {
if (label[i] == i) components++; // each component ends up labelled by its smallest vertex
}
return components;
}
Complexity. Time O(V * E): each sweep costs O(E), and the smallest label of a component can need up to V - 1 sweeps to walk to its far end, since it may advance only one edge per sweep. Space O(V) for the labels.
The worst case is easy to trigger: a path 0-1-2-...-1999 whose edges are listed from the far end, {1998, 1999}, {1997, 1998}, .... Label 0 then moves one step per sweep. The test file includes exactly this input. For V = 2000 that is a few million operations, fine here, but the bound is quadratic and we can do linear.
Approach 2: Optimal, traversal from every unvisited vertex
Intuition. A BFS (or DFS) started at a vertex visits precisely the vertices of that vertex’s component, no more and no less. So loop over all vertices in order; whenever you meet one that has not been visited yet, it must belong to a component nobody has explored, so count one and run a traversal from it to mark the whole component. Vertices already marked are skipped. Each component is therefore counted exactly once, at its first vertex in label order.
This outer loop is the important lesson of the lecture. Every graph algorithm that has to touch all vertices, cycle detection, bipartite checks, topological sort, uses the same pattern: for each vertex, if not visited, traverse from it.
Algorithm.
- Build an adjacency list, adding each edge in both directions.
- For
start = 0 .. n-1: ifstartis visited, skip it. Otherwise increment the count, markstart, and BFS from it, marking every newly reached vertex. - Return the count.
static int optimalTraversal(int n, int[][] edges) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]);
}
boolean[] visited = new boolean[n];
int components = 0;
Deque<Integer> queue = new ArrayDeque<>();
for (int start = 0; start < n; start++) {
if (visited[start]) continue; // already swallowed by an earlier component
components++;
visited[start] = true;
queue.add(start);
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : adj.get(u)) {
if (!visited[v]) {
visited[v] = true;
queue.add(v);
}
}
}
}
return components;
}
Dry run on example 2 (n = 6, triangle 0-1-2, pair 3-4, isolated 5):
| start | visited before? | action | visited after |
|---|---|---|---|
| 0 | no | count = 1, BFS reaches 0, 1, 2 | {0, 1, 2} |
| 1 | yes | skip | |
| 2 | yes | skip | |
| 3 | no | count = 2, BFS reaches 3, 4 | {0..4} |
| 4 | yes | skip | |
| 5 | no | count = 3, BFS reaches only 5 | {0..5} |
Complexity. Time O(V + E): building the list is O(V + E), every vertex enters the queue once over the whole run, and every adjacency list is scanned once. Space O(V + E) for the list, plus O(V) for the visited array and queue.
BFS is used here because it needs no recursion. A recursive DFS works just as well and is equally common; on very long paths it can overflow the call stack, which BFS never does.
Approach 3: Optimal, union-find
Intuition. Start with n separate singletons, one per vertex, so n components. Each edge either joins two different groups, merging them and reducing the count by one, or connects two vertices that are already in the same group and changes nothing. A disjoint set union (union-find) structure answers “which group is x in?” almost in constant time, so we can process the edges in any order, without building an adjacency list.
find walks up parent pointers to the root that represents the group, and path halving (pointing each visited node at its grandparent) keeps the trees shallow. Union by size hangs the smaller tree under the larger one so the trees never get deep.
Algorithm.
parent[i] = i,size[i] = 1,components = n.- For each edge
{u, v}: find the rootsaandb. If they are equal, continue. Otherwise attach the smaller tree to the larger and decrementcomponents. - Return
components.
static int optimalUnionFind(int n, int[][] edges) {
int[] parent = new int[n], size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
int components = n;
for (int[] e : edges) {
int a = find(parent, e[0]), b = find(parent, e[1]);
if (a == b) continue; // already in the same component: nothing merges
if (size[a] < size[b]) {
int t = a;
a = b;
b = t;
}
parent[b] = a; // hang the smaller tree under the larger
size[a] += size[b];
components--;
}
return components;
}
static int find(int[] parent, int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // path halving keeps the trees shallow
x = parent[x];
}
return x;
}
Complexity. Time O(V + E * α(V)), where α is the inverse Ackermann function, at most 4 for any input you will ever see. Space O(V) for the two arrays.
Union-find shines when edges arrive one by one and you need the component count after each one, or when the graph is too large to store as an adjacency list. For a single static count, the traversal is just as good.
Edge cases and pitfalls
- Isolated vertices. With no edges, the answer is
n. All three methods count a vertex that appears in no edge, because they iterate over0..n-1rather than over the vertices mentioned in the edge list. - Repeated edges.
[[0,1], [1,0], [0,1]]on three vertices is still two components. The traversal ignores already-visited neighbours, union-find sees equal roots, and label propagation simply finds nothing to change. - Self-loops. An edge
{2, 2}joins vertex 2 to itself and merges nothing. Union-find hitsa == band skips it. - Forgetting the outer loop. A single BFS from vertex 0 counts only one component. Every vertex must be tried as a start.
- Directed input. Components here are for undirected graphs. In a directed graph, “can reach each other” leads to strongly connected components, which need Kosaraju’s or Tarjan’s algorithm, not a plain traversal.
Related problems
- Number of provinces is the same count with the graph given as an adjacency matrix.
- Connected Components Problem in Matrix counts components of a grid, where neighbours are implicit.
- Traversal Techniques explains the BFS used inside the outer loop.
- Disjoint Set develops union-find in full.
- Number of operations to make network connected uses the component count directly.