323. Number of Connected Components In An Undirected Graph - Explanation

Problem Link

Description

You have a graph of n nodes. You are given an integer n and an array edges where edges[i] = [aᵢ, bᵢ] indicates that there is an edge between aᵢ and bᵢ in the graph.

Return the number of connected components in the graph.

Example 1:

Input:
n = 5, edges = [[0,1],[1,2],[3,4]]

Output: 2

Example 2:

Input:
n = 5, edges = [[0,1],[1,2],[2,3],[3,4]]

Output: 1

Constraints:

  • 1 <= n <= 2000
  • 1 <= edges.length <= 5000
  • edges[i].length == 2
  • 0 <= aᵢ <= bᵢ < n
  • aᵢ != bᵢ
  • There are no repeated edges.


Topics

Recommended Time & Space Complexity

You should aim for a solution as good or better than O(V + E) time and O(V + E) space, where V is the number vertices and E is the number of edges in the graph.


Hint 1

Assume there are no edges initially, so we have n components, as there are that many nodes given. Now, we should add the given edges between the nodes. Can you think of an algorithm to add the edges between the nodes? Also, after adding an edge, how do you determine the number of components left?


Hint 2

We can use the Union-Find (DSU) algorithm to add the given edges. For simplicity, we use Union-Find by size, where we merge the smaller component into the larger one. The Union-Find algorithm inserts the edge only between two nodes from different components. It does not add the edge if the nodes are from the same component. How do you find the number of components after adding the edges? For example, consider that nodes 0 and 1 are not connected, so there are initially two components. After adding an edge between these nodes, they become part of the same component, leaving us with one component.


Hint 3

We create an object of the DSU and initialize the result variable res = n, which indicates that there are n components initially. We then iterate through the given edges. For each edge, we attempt to connect the nodes using the union function of the DSU. If the union is successful, we decrement res; otherwise, we continue. Finally, we return res as the number of components.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Graph Representation - Building adjacency lists from edge lists for efficient traversal
  • Depth First Search (DFS) - Recursively exploring all reachable nodes from a starting point
  • Breadth First Search (BFS) - Level-by-level traversal using a queue to visit connected nodes
  • Union-Find (Disjoint Set Union) - Efficiently grouping nodes into sets with union and find operations

Intuition

A connected component is a group of nodes where every node is reachable from any other node in that group.

Using DFS:

  • If we start DFS from an unvisited node, we will visit all nodes in its connected component
  • Every time we start DFS from a new unvisited node, we’ve found one new component

Algorithm

  1. Build an adjacency list from the edges.
  2. Maintain a visited array to track visited nodes.
  3. Initialize components = 0.
  4. For each node from 0 to n-1:
    • If the node is not visited:
      • Run DFS from this node (mark all reachable nodes as visited)
      • Increment components by 1
  5. Return components.
class Solution:
    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        adj = [[] for _ in range(n)]
        visit = [False] * n
        for u, v in edges:
            adj[u].append(v)
            adj[v].append(u)

        def dfs(node):
            for nei in adj[node]:
                if not visit[nei]:
                    visit[nei] = True
                    dfs(nei)

        res = 0
        for node in range(n):
            if not visit[node]:
                visit[node] = True
                dfs(node)
                res += 1
        return res

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE is the number of edges in the graph.


Intuition

A connected component is a set of nodes where each node can reach the others.

Using BFS:

  • Starting BFS from an unvisited node will visit all nodes in that component
  • Each time we start BFS from a new unvisited node, we discover one new connected component

Algorithm

  1. Build an adjacency list from the given edges.
  2. Create a visited array to mark nodes that are already seen.
  3. Initialize components = 0.
  4. For every node from 0 to n-1:
    • If the node is not visited:
      • Start BFS from this node
      • Mark all reachable nodes as visited
      • Increment components by 1
  5. Return components.
class Solution:
    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        adj = [[] for _ in range(n)]
        visit = [False] * n
        for u, v in edges:
            adj[u].append(v)
            adj[v].append(u)

        def bfs(node):
            q = deque([node])
            visit[node] = True
            while q:
                cur = q.popleft()
                for nei in adj[cur]:
                    if not visit[nei]:
                        visit[nei] = True
                        q.append(nei)

        res = 0
        for node in range(n):
            if not visit[node]:
                bfs(node)
                res += 1
        return res

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE is the number of edges in the graph.


3. Disjoint Set Union (Rank | Size)

Intuition

Disjoint Set Union (DSU) groups nodes into connected components efficiently.

  • Start by assuming each node is its own component
  • When we process an edge (u, v):
    • If u and v are already in the same set, nothing changes
    • If they are in different sets, we merge them and the number of components decreases by 1
  • Using union by rank/size + path compression keeps operations fast

At the end, the number of remaining sets is the number of connected components.

Algorithm

  1. Initialize DSU with n nodes, each node as its own parent.
  2. Set components = n.
  3. For each edge (u, v):
    • If union(u, v) is successful (they were separate):
      • Decrement components by 1
  4. Return components.
class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [1] * n

    def find(self, node):
        cur = node
        while cur != self.parent[cur]:
            self.parent[cur] = self.parent[self.parent[cur]]
            cur = self.parent[cur]
        return cur

    def union(self, u, v):
        pu = self.find(u)
        pv = self.find(v)
        if pu == pv:
            return False
        if self.rank[pv] > self.rank[pu]:
            pu, pv = pv, pu
        self.parent[pv] = pu
        self.rank[pu] += self.rank[pv]
        return True

class Solution:
    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        dsu = DSU(n)
        res = n
        for u, v in edges:
            if dsu.union(u, v):
                res -= 1
        return res

Time & Space Complexity

  • Time complexity: O(V+(Eα(V)))O(V + (E * α(V)))
  • Space complexity: O(V)O(V)

Where VV is the number of vertices and EE is the number of edges in the graph. α()α() is used for amortized complexity.


Common Pitfalls

Forgetting Isolated Nodes

When there are no edges, each node is its own component. Solutions that only iterate through edges will miss nodes with no connections and return 0 instead of n.

# Wrong: misses isolated nodes
for u, v in edges:
    # only processes connected nodes

Building a Directed Graph Instead of Undirected

Edges must be added in both directions. Adding only adj[u].append(v) without adj[v].append(u) causes incomplete traversals and overcounts components.

Not Marking Nodes as Visited Before Exploring

In BFS/DFS, marking a node as visited only after processing (instead of when first discovered) can cause nodes to be added to the queue multiple times, leading to incorrect counts or infinite loops.