Connected Components
Partition an undirected graph into maximal groups of mutually reachable vertices with one traversal per group.
Overview
A connected component of an undirected graph is a maximal set of vertices in which every pair is joined by some path. Every vertex belongs to exactly one component, so the components partition the vertex set. A graph with a single component is called *connected*.
Finding components is the simplest use of graph traversal: start a Depth-First Search (DFS) or Breadth-First Search (BFS) from any unvisited vertex, label everything it reaches with the same id, then move on to the next unvisited vertex. Each traversal discovers exactly one component. The number of traversals started is the number of components.
For directed graphs the notion splits into weakly connected components (ignore edge direction) and Strongly Connected Components (respect direction, need Tarjan's SCC Algorithm or Kosaraju's Algorithm). This topic is about undirected graphs, or directed graphs treated as undirected.
Intuition
A mental model before the formal terms.
Picture the vertices as islands and the edges as bridges. Drop a bucket of paint on one island and let it flow across every bridge: everything that gets wet is one component. Then find a dry island and repeat with a new colour. The number of colours you used is the number of components — no colour ever leaks into another because there is, by definition, no bridge between them.
A 0/1 grid where 1 cells are land and adjacent land cells are joined ("Number of Islands") is exactly this problem; the adjacency list is implicit in the four grid directions.
How it works
- Build the adjacency list. For an undirected graph insert each edge
(u, v)in bothadj[u]andadj[v]. - Keep an array
comp[v], initialised to-1(unlabelled), and a countercount = 0. - Scan vertices
s = 0..n-1. Ifcomp[s]is already set, skip it. - Otherwise run a traversal from
s(iterative DFS with an explicit stack or BFS with a queue). Whenever you first reach a vertex, setcomp[vertex] = countand push it. Labelling on push (not pop) guarantees each vertex is pushed at most once. - When the traversal drains, increment
count. At the endcountis the number of components andcompmaps every vertex to its component id. - Alternative: a Union-Find (Disjoint Set Union) structure. Union the endpoints of every edge; the number of components is
nminus the number of successful unions. This is the natural choice when edges arrive online.
Why it works
A traversal from s visits exactly the vertices reachable from s. In an undirected graph reachability is symmetric and transitive, so "reachable from s" is precisely the component of s. No vertex outside the component can be reached (there is no edge into it), and no vertex inside can be missed (a path to it exists, and traversal follows every edge of every visited vertex).
Because every vertex is labelled once and every adjacency list is scanned once, the total work is O(V + E) regardless of how many components there are.
Recognition
How to tell a problem wants this.
- The statement asks "how many groups / islands / provinces / clusters", or whether two vertices are "in the same network".
- Relations that are symmetric (friendship, "adjacent land cells", "equations
a == b") define an undirected graph, and the question is about groups under that relation. - You need a component id per vertex to answer many connectivity queries in
O(1)afterwards. - Edges arrive one at a time and you must report the component count after each insert — use Union-Find (Disjoint Set Union) instead of re-running traversal.
Interactive visualization
Play, step, change the input. ← → and space work too.
1comp = {}; count = 02for s in nodes:3 if s in comp: continue4 count += 1; stack = [s]; comp[s] = count5 while stack not empty:6 u = stack.pop()7 for v in neighbors(u):8 if v not in comp: comp[v] = count; stack.push(v)9return countPseudocode
1build adj (both directions)2comp[0..n-1] = -1, count = 03for s in 0..n-1:4 if comp[s] != -1: continue5 comp[s] = count; stack = [s]6 while stack not empty:7 u = stack.pop()8 for w in adj[u]:9 if comp[w] == -1: comp[w] = count; stack.push(w)10 count += 111return count, compImplementations
1def connected_components(n: int, edges: list[list[int]]) -> tuple[int, list[int]]:2 """Return (number of components, comp) where comp[v] is the id of v's component."""31 · Build undirected adjacency list4 adj: list[list[int]] = [[] for _ in range(n)]5 for u, v in edges:6 adj[u].append(v)7 adj[v].append(u)8 92 · Label array and counter10 comp = [-1] * n11 count = 012 133 · Start one traversal per unlabelled vertex14 for s in range(n):15 if comp[s] != -1:16 continue17 comp[s] = count18 stack = [s]19 204 · Iterative DFS, label on push21 while stack:22 u = stack.pop()23 for w in adj[u]:24 if comp[w] == -1:25 comp[w] = count26 stack.append(w)27 285 · One component finished29 count += 130 return count, comp[[] for _ in range(n)]builds independent lists;[[]] * nwould alias one list.comp = [-1] * nis fine for immutable ints.- A plain
listis the idiomatic Python stack:append/pop()are amortised O(1). - Neighbours are labelled before
append, so no vertex is pushed twice. - Returns a tuple
(count, comp); callers unpack it.
- Use
listfor a stack andcollections.dequefor a queue;list.pop(0)is O(n). - The iterative version avoids
sys.setrecursionlimit; a recursive DFS dies at ~1000 frames by default. - Type hints
list[list[int]]are Python 3.9+ builtin generics.
adj = [[]] * ncreates one shared list.- Recursive DFS on a long path without raising the recursion limit —
RecursionError. - Marking visited on pop, which lets a vertex be pushed once per incident edge.
- All four use an explicit stack; recursion would overflow in Python (~1000 frames) and JS/TS (~10k) on path-shaped graphs, while C++ typically survives ~10^5 frames.
- C++
std::vectoris the stack; JS/TS arrays withpush/pop; Pythonlist. None should be used as a FIFO queue (shift()/pop(0)are O(n)). - C++ returns a
pair, JS/TS an object, Python a tuple.
Complexity
Union-find variant: O(E α(V)) with path compression; effectively linear.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Counting groups / islands / clusters in a static undirected graph or grid.
- Pre-computing a component id per vertex so that "are
uandvconnected?" is anO(1)array comparison. - As the first step of larger problems: e.g. solve something per component, or check that a graph is connected before running an MST or Eulerian algorithm.
- Directed graphs where direction matters — use Strongly Connected Components instead; treating edges as undirected gives only weak components.
- Edges are added incrementally with queries in between — a traversal per query is
O(V + E)each; Union-Find (Disjoint Set Union) answers in near-constant amortised time. - You need the components after deleting edges (offline: process deletions in reverse with union-find; online: much harder).
Alternatives
Common mistakes
- Marking a vertex visited on pop instead of on push in iterative DFS — a vertex can then be pushed many times and the stack blows up to
O(E). - Forgetting to insert undirected edges in both directions; components then depend on which endpoint the traversal happened to start from.
- Isolated vertices (no edges) are components too — never derive the count only from the edge list.
- Recursive DFS on a
10^5-vertex path graph overflows the stack in Python/JavaScript; use the iterative version.
Interview patterns
- Number of islands / number of provinces: flood fill on a grid or adjacency matrix.
- Count components with union-find while streaming edges; the count is
n - successfulUnions. - Check whether a graph is a tree: connected and exactly
n - 1edges. - Equations-satisfiable / accounts-merge style problems: group by symmetric relation, then process each group.
- Choosing between BFS, DFS, Dijkstra and DPAdvanced
- Stack versus queueBeginner
- Recursion versus iterationIntermediate
- When space complexity mattersIntermediate
- Course ScheduleIntermediate
- Network Delay TimeAdvanced
- Number of IslandsIntermediate