How Many Groups Right Now?

You know how to merge. You know how to detect cycles. You can compress paths and keep trees shallow. The Union-Find toolkit is complete — constructor, find, union, connected. All the verbs. But here's a question that came up in an interview and completely froze me: how many connected components does this graph have?

I had Union-Find loaded in my head. I'd just explained path compression. The interviewer nodded, seemed satisfied, then drew a graph on the whiteboard — maybe 8 nodes, a handful of edges — and asked: “After processing all these edges, how many groups are there?” And I went blank. Not because the question was hard. Because I'd never thought about the count.

Every Union-Find tutorial I'd read focused on the operations — union two things, find the root, check if connected. The result was always binary: same group or different. But nobody talked about the aggregate view. How many groups exist right now? After 12 edges, how many connected components remain? If I add one more edge, does the count change?

The answer is embarrassingly simple once you see it. But getting there requires noticing something you've been doing all along without naming it.

Think back to every union() call you've made. Sometimes the union did something — two different trees merged into one. Other times, the union was a no-op — both nodes already shared a root, so nothing changed. You even learned to detect that case: find(u) === find(v) means “already connected” (and in a different context, “cycle detected”). But you've been treating that check as a boolean answer. Same or different. Yes or no.

What if you counted instead?

You start with n nodes. Each one is its own component — that's the self-loop initialization from the very first lesson. n nodes, n components. Then edges arrive. Each edge either merges two different components (and the count drops by one) or connects two nodes that already share a root (and the count stays the same). The merge count tracks exactly how many times the forest got simpler.

But I don't want to just tell you the formula. I want you to see it emerge. Below, you'll start with a messy forest — chains and stars mixed together — and try to count the roots by eye. Then you'll watch edges arrive one at a time, predicting what happens to the component count after each. By the time you build the algorithm yourself, the formula will feel obvious. It should feel like something you already knew.

The counting algorithm

Phase: Identify the roots in this forest
This forest has chains and stars mixed together. Tap all the ROOT nodesnodes that point to themselves.
0123456
0 nodes selected as roots

The Formula

components = n - successful_merges

That's it. Start at n — every node alone. Each time find(u) !== find(v) and you perform a union, the count drops by exactly one. Two groups become one. Redundant edges (same root) change nothing. The count at the end is whatever n minus the number of merges that actually happened.

This is the same check you've been using since SC-E for cycle detection. find(u) === find(v) meant “cycle detected — skip this edge.” Now it means “redundant edge — count unchanged.” Same code, different lens. The connected-components counter and the cycle detector are the same if statement.

Here's the complete function:

1
function countComponents(n: number, edges: number[][]): number {
2
  const parent = Array.from({length: n}, (_, i) => i);
3
  const rank = new Array(n).fill(0);
4
  let components = n;
5
6
  for (const [u, v] of edges) {
7
    if (find(u) !== find(v)) {
8
      union(u, v);
9
      components--;
10
    }
11
  }
12
13
  return components;
14
}

The components-- inside the if is the entire counting logic. No separate pass over the array. No BFS. No visited set. You count as you merge, because the count IS the merge history.

components =6-0=6
Next: edge 0–1

This pattern appears everywhere:

  • LC 323 — Number of Connected Components (this exact problem)
  • LC 200 — Number of Islands (grid version — each cell is a node, adjacent land cells get unioned)
  • LC 547 — Number of Provinces (adjacency matrix — isConnected[i][j] means union i and j)
  • LC 1319 — Number of Operations to Make Network Connected (if edges >= n-1, answer is components - 1)

The grid variant (LC 200) is worth noting. You can solve “Number of Islands” with BFS or DFSflood-fill each unvisited land cell and count the floods. That's the classic approach and it's perfectly fine. But Union-Find also works: scan each cell, union it with its land neighbors, and the component count at the end is the island count. Same answer, different tool.

So when do you reach for Union-Find over BFS/DFS?

Union-Find wins when edges arrive dynamically — in a stream, one at a time — and you need the component count after each insertion. BFS/DFS would require a full re-traversal after every new edge. Union-Find handles each edge in near-O(1) amortized time and the count is always available.

BFS/DFS wins when you need to enumerate the reachable nodes from a source, not just count groups. Union-Find answers “are X and Y connected?” but cannot list all nodes reachable from X without scanning the entire parent array.

Both work when you have a static graph and just need the total component count. For a graph with V vertices and E edges, both approaches are O(V + E). Pick whichever you find clearer.

0-1
2-3
4-5
6-7
1-2
5-6
0-3
4-7
Nodes visited per edgeTotal: 0

This module has taken you through the full Union-Find journey:

  • SC-A: The Forest of Strangersparent[i] = i creates N independent trees. The self-loop IS the root marker.
  • SC-B: The Long Walk — Naive find() walks chains at O(n) cost. Shape determines speed.
  • SC-C: The Shortcut — Path compression flattens the chain you walk. Local, lazy, nearly free.
  • SC-D: Rank and File — Union by rank keeps trees shallow. Attach the shorter tree under the taller one.
  • SC-E: The Redundancy Checkfind(u) === find(v) before union = cycle detection. Skip the edge.
  • SC-F: The Head Countcomponents = n - merges. Count as you go. No second pass needed.

Six lessons. One data structure. From a buggy constructor to a complete toolkit that handles connectivity, cycle detection, and component counting — all through the same parent-pointer forest you built in lesson one.

The next time an interviewer asks “how many groups?”, you won't freeze. You'll say: "Start at n, decrement on every successful merge, return what's left." And you'll mean it, because you built the formula yourself, edge by edge.