You're handed a social network of n people and a list of friendships. Some people form a tight clique where everyone is reachable through a chain of mutual friends; others sit in their own little group, or alone. A connected component is one such island — a maximal set of nodes where you can walk from any node to any other by following edges. Your job is to count how many islands the graph splits into.
Implement graphCountConnectedComponents(n, edges). The graph is undirected: an edge [u, v] means u and v are mutually connected, with no direction. Nodes are labelled 0 through n - 1.
// n: number — the count of nodes, labelled 0, 1, ..., n - 1.
// edges: Array<[number, number]> — each pair [u, v] connects node u and node v
// (undirected: [u, v] and [v, u] mean the same thing).
// returns: number — how many connected components the graph has.
function graphCountConnectedComponents(n: number, edges: number[][]): number;
// Five nodes. Edges link 0-1-2 into one island and 3-4 into another.
// Node layout: 0 - 1 - 2 3 - 4
graphCountConnectedComponents(5, [
[0, 1],
[1, 2],
[3, 4],
]); // → 2
// No edges at all: every node stands alone, so each is its own component.
graphCountConnectedComponents(4, []); // → 4
// Two pairs and one node nobody is connected to.
// Node layout: 0 - 1 2 - 3 4
graphCountConnectedComponents(5, [
[0, 1],
[2, 3],
]); // → 3
[u, v] connects both ways. When you build your graph, record u as a neighbour of v and v as a neighbour of u.0..n-1. Every label in that range is a real node, even if no edge ever mentions it. n is the source of truth for how many nodes exist — not the edge list.n = 3 and no edges, the answer is 3.n = 0 returns 0. No nodes, no components.[2, 2] or the same pair twice. You don't need to defend against them — though, as it happens, the standard solutions handle both gracefully.You'll count the separate "islands" in an undirected graph by walking every island fully, one at a time, and tallying how many walks it takes to cover all the nodes.
Picture a room of n people and a list of who knows whom. Two people are in the same friend circle if you can get from one to the other by hopping along a chain of mutual friends — directly or through intermediaries. A connected component is one complete friend circle. You want to know how many circles the room breaks into. Someone who knows nobody is a circle of one, so they still count. The number you return is the count of those circles, nothing more.
A component is a maximal reachable set: pick any node, follow edges as far as they go, and the set of everything you touch is exactly one component. The trick to counting components is to flood-fill one completely, mark every node in it as seen, then look for any node you haven't seen yet. Each time you find a fresh unseen node, that's the start of a brand-new component — so you bump the counter and flood-fill again. When no unseen nodes remain, the counter holds the answer.
Before any traversal, you need the graph in a shape you can walk. The input is a flat list of [u, v] pairs. The natural structure for "who are this node's neighbours?" is an adjacency list: an array where index i holds the list of nodes directly connected to i. Because the graph is undirected, every edge has to be recorded from both ends.
When people first see "count the components," two wrong instincts show up. Both are worth writing out, because the fix for each points straight at the real solution.
Attempt 1 — count from the edges. It's tempting to think the structure is in the edge list, so maybe the answer is some arithmetic on it. A common guess: "each edge joins two nodes, so it removes one component; start at n components and subtract one per edge."
function byEdgeCount(n, edges) {
// Start with n lone nodes; assume each edge fuses two components into one.
return n - edges.length;
}
This is right only when the edges form no cycles — a forest. The moment two nodes that are already in the same component get another edge between them, that edge joins nothing, but we still subtract one. Take the triangle [[0, 1], [1, 2], [2, 0]] with n = 3: it's a single component, but byEdgeCount returns 3 - 3 = 0. Worse, with extra edges inside one group it can go negative. Edge count can't tell a merging edge from a redundant one — you have to look at which nodes an edge touches and whether they're already linked.
Attempt 2 — flood-fill, but forget to track what you've already seen. This instinct is much closer: traverse the graph from node 0, and count. But if you don't remember which nodes you've already visited, the count goes wrong in two directions at once.
function noVisited(n, edges) {
const adj = Array.from({ length: n }, () => []);
for (const [u, v] of edges) {
adj[u].push(v);
adj[v].push(u);
}
let count = 0;
for (let node = 0; node < n; node++) {
// No record of visited nodes, so EVERY node looks like a new component...
count++;
}
return count; // always returns n
}
Without a visited record, every node in the outer loop looks brand-new, so this just returns n every time — it never notices that node 1 was already swept up when we explored node 0. And if you instead try to flood from a single start node without marking visits, a cycle like 0 - 1 - 2 - 0 sends the traversal around forever. The missing piece in both failures is the same: a visited set that flood-fill writes to and the outer scan reads from.
function graphCountConnectedComponents(n, edges) {
// Build an adjacency list: index i holds an array of i's neighbours.
const adj = Array.from({ length: n }, () => []);
for (const [u, v] of edges) {
adj[u].push(v); // the edge connects u and v...
adj[v].push(u); // ...and, because it's undirected, v and u.
}
const visited = new Array(n).fill(false);
let count = 0;
// Iterative DFS that marks every node reachable from `start`.
function explore(start) {
const stack = [start];
visited[start] = true;
while (stack.length > 0) {
const node = stack.pop();
for (const next of adj[node]) {
if (!visited[next]) {
visited[next] = true;
stack.push(next);
}
}
}
}
// Every node not yet reached starts a brand-new component.
for (let node = 0; node < n; node++) {
if (!visited[node]) {
count++;
explore(node);
}
}
return count;
}
module.exports = { graphCountConnectedComponents };
Three pieces carry the weight; take each in turn.
Why we mark visited[next] = true at push time, not at pop time. A node can be reachable from several of its neighbours at once. If you only marked a node visited when you popped it off the stack, the same node could be pushed two or three times before its first pop — bloating the stack and, in a dense graph, re-scanning its whole neighbour list each time. Marking it the instant you decide to push guarantees every node enters the stack exactly once. (The explore(start) call marks the start node up front for the same reason.)
Why the visited flag is shared across every call to explore. This is the line that fixes Attempt 2. visited is declared once, outside the function, so a flood-fill that began at node 0 leaves its marks behind for the outer loop to read. When the loop later reaches node 1, visited[1] is already true — it was swept up by the first flood — so we don't count it again. Only a node that no previous flood reached survives the if (!visited[node]) check, and that's exactly the definition of "the first node of a new component."
Why the counter increments in the outer loop, not inside explore. Each iteration of the outer loop that passes the !visited guard is, by definition, the discovery of a component nobody has touched yet. That's the one place a count++ belongs. explore then does the grunt work of marking the entire component so the rest of its nodes are skipped. The loop runs n times total, but count ticks up only when a genuinely new island appears.
The shift from the naive versions is small but decisive: we kept Attempt 2's "scan every node and flood-fill" shape, and added the one ingredient it lacked — a visited array that the flood writes and the scan reads. That single shared piece of state turns "count every node" into "count every component."
Let's run graphCountConnectedComponents(5, [[0, 1], [1, 2], [3, 4]]). The graph is 0 - 1 - 2 in one island and 3 - 4 in another, with the expected answer 2.
First we build adj:
adj[0] = [1]
adj[1] = [0, 2]
adj[2] = [1]
adj[3] = [4]
adj[4] = [3]
visited = [false, false, false, false, false], count = 0. Now the outer loop:
node = 0 → visited[0] is false → count = 1, explore(0)
stack [0], mark 0. pop 0, neighbours [1]: 1 unseen → mark 1, push.
stack [1]. pop 1, neighbours [0, 2]: 0 seen, 2 unseen → mark 2, push.
stack [2]. pop 2, neighbours [1]: 1 seen → nothing.
stack empty → done. visited = [T, T, T, F, F]
node = 1 → visited[1] is true → skip
node = 2 → visited[2] is true → skip
node = 3 → visited[3] is false → count = 2, explore(3)
stack [3], mark 3. pop 3, neighbours [4]: 4 unseen → mark 4, push.
stack [4]. pop 4, neighbours [3]: 3 seen → nothing.
stack empty → done. visited = [T, T, T, T, T]
node = 4 → visited[4] is true → skip
return count = 2
The two count++ moments — at node = 0 and node = 3 — are precisely the two times the scan landed on a node no earlier flood had reached. Nodes 1, 2, and 4 were all marked by a flood before the loop reached them, so none of them triggered a recount.
visited entirely. Without it the outer loop counts all n nodes as separate components (it never learns that node 1 was reached while flooding from 0), and a cycle like 0 - 1 - 2 - 0 makes a naive flood loop forever. Fix: one shared visited array — flood-fill writes to it, the outer scan reads from it.adj[u].push(v) but skip adj[v].push(u), then starting a flood at v can't reach u, and you over-count. For [[0, 1]] with n = 2, a one-directional list gives adj[1] = [], so flooding from 1 never reaches 0 — answer 2 instead of 1. Fix: push both directions for every edge.n. A node like 5 with no edges never appears in the edge list. If you build adj by scanning edges and only create entries for nodes you see, node 5 silently vanishes and you under-count. Fix: adj has exactly n slots from the start (Array.from({ length: n }, () => [])), and the outer loop runs 0 to n - 1 — isolated nodes get counted because the loop visits them even though no edge mentions them.explore instead of in the outer loop. If you do count++ on every node you flood, you're back to counting nodes, not components — the answer becomes n again. Fix: increment exactly once per flood, at the outer-loop site where a fresh unvisited node is discovered.node < n, and labels run 0..n-1. Writing node <= n reads adj[n], which is undefined, and iterating it throws. With n = 0 the loop body never runs and you correctly return 0. Fix: half-open range 0 to n, never inclusive of n.explore is shorter, but on a long chain (0 - 1 - 2 - ... - n) it recurses n deep and can overflow the call stack for large n. The explicit stack array here sidesteps that — it's the same depth-first walk with the recursion made manual.count = n and treat each node as its own set. For every edge [u, v], find the representative ("root") of u's set and of v's set; if they differ, merge the two sets and do count--. An edge between two nodes already in the same set merges nothing and leaves count alone. After processing all edges, count is the answer. With path compression and union by rank, each operation is effectively constant time, giving near-linear total work — and it shines when edges stream in and you want a running component count without re-traversing.(r, c) are its four grid neighbours that are also land. Swap the adjacency list for "look up, down, left, right" and the count-and-flood logic is identical.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
You're handed a social network of n people and a list of friendships. Some people form a tight clique where everyone is reachable through a chain of mutual friends; others sit in their own little group, or alone. A connected component is one such island — a maximal set of nodes where you can walk from any node to any other by following edges. Your job is to count how many islands the graph splits into.
Implement graphCountConnectedComponents(n, edges). The graph is undirected: an edge [u, v] means u and v are mutually connected, with no direction. Nodes are labelled 0 through n - 1.
// n: number — the count of nodes, labelled 0, 1, ..., n - 1.
// edges: Array<[number, number]> — each pair [u, v] connects node u and node v
// (undirected: [u, v] and [v, u] mean the same thing).
// returns: number — how many connected components the graph has.
function graphCountConnectedComponents(n: number, edges: number[][]): number;
// Five nodes. Edges link 0-1-2 into one island and 3-4 into another.
// Node layout: 0 - 1 - 2 3 - 4
graphCountConnectedComponents(5, [
[0, 1],
[1, 2],
[3, 4],
]); // → 2
// No edges at all: every node stands alone, so each is its own component.
graphCountConnectedComponents(4, []); // → 4
// Two pairs and one node nobody is connected to.
// Node layout: 0 - 1 2 - 3 4
graphCountConnectedComponents(5, [
[0, 1],
[2, 3],
]); // → 3
[u, v] connects both ways. When you build your graph, record u as a neighbour of v and v as a neighbour of u.0..n-1. Every label in that range is a real node, even if no edge ever mentions it. n is the source of truth for how many nodes exist — not the edge list.n = 3 and no edges, the answer is 3.n = 0 returns 0. No nodes, no components.[2, 2] or the same pair twice. You don't need to defend against them — though, as it happens, the standard solutions handle both gracefully.You'll count the separate "islands" in an undirected graph by walking every island fully, one at a time, and tallying how many walks it takes to cover all the nodes.
Picture a room of n people and a list of who knows whom. Two people are in the same friend circle if you can get from one to the other by hopping along a chain of mutual friends — directly or through intermediaries. A connected component is one complete friend circle. You want to know how many circles the room breaks into. Someone who knows nobody is a circle of one, so they still count. The number you return is the count of those circles, nothing more.
A component is a maximal reachable set: pick any node, follow edges as far as they go, and the set of everything you touch is exactly one component. The trick to counting components is to flood-fill one completely, mark every node in it as seen, then look for any node you haven't seen yet. Each time you find a fresh unseen node, that's the start of a brand-new component — so you bump the counter and flood-fill again. When no unseen nodes remain, the counter holds the answer.
Before any traversal, you need the graph in a shape you can walk. The input is a flat list of [u, v] pairs. The natural structure for "who are this node's neighbours?" is an adjacency list: an array where index i holds the list of nodes directly connected to i. Because the graph is undirected, every edge has to be recorded from both ends.
When people first see "count the components," two wrong instincts show up. Both are worth writing out, because the fix for each points straight at the real solution.
Attempt 1 — count from the edges. It's tempting to think the structure is in the edge list, so maybe the answer is some arithmetic on it. A common guess: "each edge joins two nodes, so it removes one component; start at n components and subtract one per edge."
function byEdgeCount(n, edges) {
// Start with n lone nodes; assume each edge fuses two components into one.
return n - edges.length;
}
This is right only when the edges form no cycles — a forest. The moment two nodes that are already in the same component get another edge between them, that edge joins nothing, but we still subtract one. Take the triangle [[0, 1], [1, 2], [2, 0]] with n = 3: it's a single component, but byEdgeCount returns 3 - 3 = 0. Worse, with extra edges inside one group it can go negative. Edge count can't tell a merging edge from a redundant one — you have to look at which nodes an edge touches and whether they're already linked.
Attempt 2 — flood-fill, but forget to track what you've already seen. This instinct is much closer: traverse the graph from node 0, and count. But if you don't remember which nodes you've already visited, the count goes wrong in two directions at once.
function noVisited(n, edges) {
const adj = Array.from({ length: n }, () => []);
for (const [u, v] of edges) {
adj[u].push(v);
adj[v].push(u);
}
let count = 0;
for (let node = 0; node < n; node++) {
// No record of visited nodes, so EVERY node looks like a new component...
count++;
}
return count; // always returns n
}
Without a visited record, every node in the outer loop looks brand-new, so this just returns n every time — it never notices that node 1 was already swept up when we explored node 0. And if you instead try to flood from a single start node without marking visits, a cycle like 0 - 1 - 2 - 0 sends the traversal around forever. The missing piece in both failures is the same: a visited set that flood-fill writes to and the outer scan reads from.
function graphCountConnectedComponents(n, edges) {
// Build an adjacency list: index i holds an array of i's neighbours.
const adj = Array.from({ length: n }, () => []);
for (const [u, v] of edges) {
adj[u].push(v); // the edge connects u and v...
adj[v].push(u); // ...and, because it's undirected, v and u.
}
const visited = new Array(n).fill(false);
let count = 0;
// Iterative DFS that marks every node reachable from `start`.
function explore(start) {
const stack = [start];
visited[start] = true;
while (stack.length > 0) {
const node = stack.pop();
for (const next of adj[node]) {
if (!visited[next]) {
visited[next] = true;
stack.push(next);
}
}
}
}
// Every node not yet reached starts a brand-new component.
for (let node = 0; node < n; node++) {
if (!visited[node]) {
count++;
explore(node);
}
}
return count;
}
module.exports = { graphCountConnectedComponents };
Three pieces carry the weight; take each in turn.
Why we mark visited[next] = true at push time, not at pop time. A node can be reachable from several of its neighbours at once. If you only marked a node visited when you popped it off the stack, the same node could be pushed two or three times before its first pop — bloating the stack and, in a dense graph, re-scanning its whole neighbour list each time. Marking it the instant you decide to push guarantees every node enters the stack exactly once. (The explore(start) call marks the start node up front for the same reason.)
Why the visited flag is shared across every call to explore. This is the line that fixes Attempt 2. visited is declared once, outside the function, so a flood-fill that began at node 0 leaves its marks behind for the outer loop to read. When the loop later reaches node 1, visited[1] is already true — it was swept up by the first flood — so we don't count it again. Only a node that no previous flood reached survives the if (!visited[node]) check, and that's exactly the definition of "the first node of a new component."
Why the counter increments in the outer loop, not inside explore. Each iteration of the outer loop that passes the !visited guard is, by definition, the discovery of a component nobody has touched yet. That's the one place a count++ belongs. explore then does the grunt work of marking the entire component so the rest of its nodes are skipped. The loop runs n times total, but count ticks up only when a genuinely new island appears.
The shift from the naive versions is small but decisive: we kept Attempt 2's "scan every node and flood-fill" shape, and added the one ingredient it lacked — a visited array that the flood writes and the scan reads. That single shared piece of state turns "count every node" into "count every component."
Let's run graphCountConnectedComponents(5, [[0, 1], [1, 2], [3, 4]]). The graph is 0 - 1 - 2 in one island and 3 - 4 in another, with the expected answer 2.
First we build adj:
adj[0] = [1]
adj[1] = [0, 2]
adj[2] = [1]
adj[3] = [4]
adj[4] = [3]
visited = [false, false, false, false, false], count = 0. Now the outer loop:
node = 0 → visited[0] is false → count = 1, explore(0)
stack [0], mark 0. pop 0, neighbours [1]: 1 unseen → mark 1, push.
stack [1]. pop 1, neighbours [0, 2]: 0 seen, 2 unseen → mark 2, push.
stack [2]. pop 2, neighbours [1]: 1 seen → nothing.
stack empty → done. visited = [T, T, T, F, F]
node = 1 → visited[1] is true → skip
node = 2 → visited[2] is true → skip
node = 3 → visited[3] is false → count = 2, explore(3)
stack [3], mark 3. pop 3, neighbours [4]: 4 unseen → mark 4, push.
stack [4]. pop 4, neighbours [3]: 3 seen → nothing.
stack empty → done. visited = [T, T, T, T, T]
node = 4 → visited[4] is true → skip
return count = 2
The two count++ moments — at node = 0 and node = 3 — are precisely the two times the scan landed on a node no earlier flood had reached. Nodes 1, 2, and 4 were all marked by a flood before the loop reached them, so none of them triggered a recount.
visited entirely. Without it the outer loop counts all n nodes as separate components (it never learns that node 1 was reached while flooding from 0), and a cycle like 0 - 1 - 2 - 0 makes a naive flood loop forever. Fix: one shared visited array — flood-fill writes to it, the outer scan reads from it.adj[u].push(v) but skip adj[v].push(u), then starting a flood at v can't reach u, and you over-count. For [[0, 1]] with n = 2, a one-directional list gives adj[1] = [], so flooding from 1 never reaches 0 — answer 2 instead of 1. Fix: push both directions for every edge.n. A node like 5 with no edges never appears in the edge list. If you build adj by scanning edges and only create entries for nodes you see, node 5 silently vanishes and you under-count. Fix: adj has exactly n slots from the start (Array.from({ length: n }, () => [])), and the outer loop runs 0 to n - 1 — isolated nodes get counted because the loop visits them even though no edge mentions them.explore instead of in the outer loop. If you do count++ on every node you flood, you're back to counting nodes, not components — the answer becomes n again. Fix: increment exactly once per flood, at the outer-loop site where a fresh unvisited node is discovered.node < n, and labels run 0..n-1. Writing node <= n reads adj[n], which is undefined, and iterating it throws. With n = 0 the loop body never runs and you correctly return 0. Fix: half-open range 0 to n, never inclusive of n.explore is shorter, but on a long chain (0 - 1 - 2 - ... - n) it recurses n deep and can overflow the call stack for large n. The explicit stack array here sidesteps that — it's the same depth-first walk with the recursion made manual.count = n and treat each node as its own set. For every edge [u, v], find the representative ("root") of u's set and of v's set; if they differ, merge the two sets and do count--. An edge between two nodes already in the same set merges nothing and leaves count alone. After processing all edges, count is the answer. With path compression and union by rank, each operation is effectively constant time, giving near-linear total work — and it shines when edges stream in and you want a running component count without re-traversing.(r, c) are its four grid neighbours that are also land. Swap the adjacency list for "look up, down, left, right" and the count-and-flood logic is identical.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.