2316. Count Unreachable Pairs of Nodes in an Undirected Graph
Problem Description
You have an undirected graph with n
nodes labeled from 0
to n - 1
. The graph's connections are given as a list of edges, where each edge [ai, bi]
represents an undirected connection between nodes ai
and bi
.
Your task is to find how many pairs of nodes cannot reach each other. Two nodes are considered unreachable from each other if there is no path connecting them through the edges in the graph.
For example, if you have nodes in separate connected components (isolated groups of connected nodes), any node from one component cannot reach any node from a different component. You need to count all such unreachable pairs.
The solution uses depth-first search (DFS) to identify connected components in the graph. The key insight is that nodes within the same connected component can all reach each other, but nodes in different components cannot.
The algorithm works by:
- Building an adjacency list representation of the graph from the edges
- Using DFS to find each connected component and count its size
t
- For each new component found, calculating the unreachable pairs as
s * t
, wheres
is the total number of nodes in all previously found components - Adding the current component's size to the running total
s
This approach efficiently counts all unreachable pairs by recognizing that each node in a new component forms an unreachable pair with every node in all previously processed components.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly involves an undirected graph with
n
nodes and edges connecting them.
Is it a tree?
- No: The graph is not necessarily a tree. It can have multiple disconnected components, and there's no guarantee of a single connected acyclic structure.
Is the problem related to directed acyclic graphs?
- No: The problem deals with an undirected graph, not a directed one.
Is the problem related to shortest paths?
- No: We're not finding shortest paths between nodes. We're identifying which nodes cannot reach each other at all.
Does the problem involve connectivity?
- Yes: The core of the problem is about connectivity - specifically finding pairs of nodes that are not connected (unreachable from each other).
Disjoint Set Union
- While DSU could solve this problem, the given solution uses DFS instead. Both approaches work for connectivity problems.
Alternative path through small constraints:
- The problem can also be solved with DFS by exploring each connected component, which is what the solution implements.
Conclusion: The flowchart leads us to connectivity problems, which can be solved using either DSU or DFS. The provided solution uses DFS to identify connected components and count unreachable pairs by tracking the size of each component and calculating cross-component pairs.
Intuition
The key insight is recognizing that in an undirected graph, nodes can be grouped into connected components. Within each connected component, all nodes can reach each other through some path. However, nodes in different components have no path between them - they are unreachable from each other.
Think of it like islands in an ocean. People on the same island can walk to meet each other, but people on different islands cannot reach each other without a bridge. We need to count how many pairs of people are on different islands.
When we find a connected component of size t
, we need to count how many unreachable pairs this component creates with all previously discovered components. If we've already found components with a total of s
nodes, then each of the t
nodes in the current component forms an unreachable pair with each of the s
nodes from previous components. This gives us s * t
new unreachable pairs.
For example, if we have three components with sizes 3, 2, and 4:
- When we process the first component (size 3), there are no previous nodes, so no pairs yet
- When we process the second component (size 2), each of its 2 nodes pairs with each of the 3 nodes from the first component:
3 * 2 = 6
pairs - When we process the third component (size 4), each of its 4 nodes pairs with each of the 5 nodes from previous components:
5 * 4 = 20
pairs - Total:
0 + 6 + 20 = 26
unreachable pairs
This approach avoids double-counting because we only count pairs between the current component and all previous ones, never counting the same pair twice. DFS naturally helps us explore each component fully before moving to the next one.
Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.
Solution Approach
The solution implements DFS to find connected components and calculate unreachable pairs efficiently.
Graph Representation:
First, we build an adjacency list g
from the edge list. For each edge [a, b]
, we add b
to g[a]
and a
to g[b]
since the graph is undirected.
DFS Function:
The dfs(i)
function explores a connected component starting from node i
:
- If the node is already visited (
vis[i]
is True), return 0 - Mark the node as visited
- Recursively visit all neighbors and count the total nodes in this component
- Return
1
(current node) plus the sum of all nodes found through neighbors
Main Algorithm:
- Initialize a visited array
vis
of sizen
with all False values - Initialize
ans = 0
(total unreachable pairs) ands = 0
(cumulative nodes processed) - For each node
i
from 0 to n-1:- Call
dfs(i)
to get the component sizet
- If
t > 0
(meaning this is a new unvisited component):- Add
s * t
toans
(pairs between this component and all previous ones) - Add
t
tos
(update the cumulative count)
- Add
- Call
Why This Works:
- Each call to
dfs(i)
that returns a non-zero value represents a new connected component - The multiplication
s * t
counts all pairs where one node is from the current component (sizet
) and the other is from any previous component (total sizes
) - By processing components one by one and only counting pairs with previous components, we avoid double-counting
- The visited array ensures each node is processed exactly once
Time Complexity: O(n + m)
where n
is the number of nodes and m
is the number of edges, as we visit each node and edge once.
Space Complexity: O(n)
for the adjacency list, visited array, and recursion stack.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with 7 nodes and edges: [[0,1], [0,2], [3,4], [5,6]]
This creates a graph with three connected components:
- Component 1: nodes {0, 1, 2}
- Component 2: nodes {3, 4}
- Component 3: nodes {5, 6}
Step 1: Build adjacency list
g[0] = [1, 2] g[1] = [0] g[2] = [0] g[3] = [4] g[4] = [3] g[5] = [6] g[6] = [5]
Step 2: Initialize variables
vis = [False, False, False, False, False, False, False]
ans = 0
(unreachable pairs count)s = 0
(cumulative nodes processed)
Step 3: Process each node
Processing node 0:
dfs(0)
explores nodes 0β1β2, returnst = 3
- Since this is the first component,
s = 0
- Add to answer:
ans += 0 * 3 = 0
- Update cumulative:
s = 0 + 3 = 3
vis = [True, True, True, False, False, False, False]
Processing nodes 1, 2:
- Already visited,
dfs
returns 0, skip
Processing node 3:
dfs(3)
explores nodes 3β4, returnst = 2
- Current
s = 3
(nodes from first component) - Add to answer:
ans += 3 * 2 = 6
(each of 2 nodes pairs with each of 3 previous nodes) - Update cumulative:
s = 3 + 2 = 5
vis = [True, True, True, True, True, False, False]
Processing node 4:
- Already visited, skip
Processing node 5:
dfs(5)
explores nodes 5β6, returnst = 2
- Current
s = 5
(nodes from first two components) - Add to answer:
ans += 5 * 2 = 10
- Update cumulative:
s = 5 + 2 = 7
vis = [True, True, True, True, True, True, True]
Processing node 6:
- Already visited, skip
Final Result: ans = 0 + 6 + 10 = 16
unreachable pairs
We can verify this: Component 1 (3 nodes) can't reach Component 2 (2 nodes) = 3Γ2 = 6 pairs. Component 1 can't reach Component 3 (2 nodes) = 3Γ2 = 6 pairs. Component 2 can't reach Component 3 = 2Γ2 = 4 pairs. Total = 6 + 6 + 4 = 16 pairs.
Solution Implementation
1class Solution:
2 def countPairs(self, n: int, edges: List[List[int]]) -> int:
3 """
4 Count the number of pairs of nodes that are not connected.
5 Two nodes are connected if there's a path between them.
6
7 Args:
8 n: Number of nodes (0 to n-1)
9 edges: List of edges connecting nodes
10
11 Returns:
12 Number of pairs of nodes in different connected components
13 """
14
15 def dfs(node: int) -> int:
16 """
17 Depth-first search to find size of connected component.
18
19 Args:
20 node: Current node to explore
21
22 Returns:
23 Size of the component containing this node (0 if already visited)
24 """
25 # If node is already visited, return 0
26 if visited[node]:
27 return 0
28
29 # Mark current node as visited
30 visited[node] = True
31
32 # Count current node (1) plus sizes of all connected components
33 component_size = 1
34 for neighbor in adjacency_list[node]:
35 component_size += dfs(neighbor)
36
37 return component_size
38
39 # Build adjacency list representation of the graph
40 adjacency_list = [[] for _ in range(n)]
41 for node_a, node_b in edges:
42 adjacency_list[node_a].append(node_b)
43 adjacency_list[node_b].append(node_a)
44
45 # Track visited nodes
46 visited = [False] * n
47
48 # Count pairs between different components
49 total_pairs = 0
50 nodes_processed = 0
51
52 for node in range(n):
53 # Get size of current component (0 if node already visited)
54 component_size = dfs(node)
55
56 # Add pairs between current component and all previous components
57 # Each node in current component can pair with each processed node
58 total_pairs += nodes_processed * component_size
59
60 # Update total nodes processed
61 nodes_processed += component_size
62
63 return total_pairs
64
1class Solution {
2 // Adjacency list to represent the graph
3 private List<Integer>[] graph;
4 // Visited array to track which nodes have been visited
5 private boolean[] visited;
6
7 /**
8 * Counts the number of unreachable pairs of nodes in the graph.
9 * @param n The number of nodes in the graph
10 * @param edges The edges connecting nodes
11 * @return The total count of unreachable pairs
12 */
13 public long countPairs(int n, int[][] edges) {
14 // Initialize the graph as an array of lists
15 graph = new List[n];
16 visited = new boolean[n];
17
18 // Create an empty list for each node
19 Arrays.setAll(graph, i -> new ArrayList<>());
20
21 // Build the undirected graph by adding edges in both directions
22 for (int[] edge : edges) {
23 int nodeA = edge[0];
24 int nodeB = edge[1];
25 graph[nodeA].add(nodeB);
26 graph[nodeB].add(nodeA);
27 }
28
29 long totalUnreachablePairs = 0;
30 long processedNodes = 0;
31
32 // Process each connected component
33 for (int i = 0; i < n; ++i) {
34 // Get the size of the current connected component
35 int componentSize = dfs(i);
36
37 // Calculate unreachable pairs between this component and previous components
38 // Each node in current component can't reach any node in previous components
39 totalUnreachablePairs += processedNodes * componentSize;
40
41 // Update the count of processed nodes
42 processedNodes += componentSize;
43 }
44
45 return totalUnreachablePairs;
46 }
47
48 /**
49 * Performs depth-first search to find the size of a connected component.
50 * @param node The starting node for DFS
51 * @return The number of nodes in the connected component
52 */
53 private int dfs(int node) {
54 // If already visited, this node is not part of a new component
55 if (visited[node]) {
56 return 0;
57 }
58
59 // Mark the current node as visited
60 visited[node] = true;
61
62 // Count the current node
63 int componentNodeCount = 1;
64
65 // Recursively visit all neighbors and add their counts
66 for (int neighbor : graph[node]) {
67 componentNodeCount += dfs(neighbor);
68 }
69
70 return componentNodeCount;
71 }
72}
73
1class Solution {
2public:
3 long long countPairs(int n, vector<vector<int>>& edges) {
4 // Build adjacency list representation of the graph
5 vector<vector<int>> adjacencyList(n);
6 for (const auto& edge : edges) {
7 int nodeA = edge[0];
8 int nodeB = edge[1];
9 adjacencyList[nodeA].push_back(nodeB);
10 adjacencyList[nodeB].push_back(nodeA);
11 }
12
13 // Track visited nodes during DFS traversal
14 vector<bool> visited(n, false);
15
16 // DFS function to count nodes in a connected component
17 function<int(int)> depthFirstSearch = [&](int currentNode) -> int {
18 // If already visited, return 0 to avoid double counting
19 if (visited[currentNode]) {
20 return 0;
21 }
22
23 // Mark current node as visited
24 visited[currentNode] = true;
25
26 // Start with count of 1 for current node
27 int componentSize = 1;
28
29 // Recursively visit all neighbors and accumulate their counts
30 for (int neighbor : adjacencyList[currentNode]) {
31 componentSize += depthFirstSearch(neighbor);
32 }
33
34 return componentSize;
35 };
36
37 // Calculate total pairs between different components
38 long long totalPairs = 0;
39 long long processedNodes = 0; // Number of nodes processed so far
40
41 for (int startNode = 0; startNode < n; ++startNode) {
42 // Get size of current component (0 if node already visited)
43 int currentComponentSize = depthFirstSearch(startNode);
44
45 // Add pairs between current component and all previous components
46 // Each node in current component can pair with each processed node
47 totalPairs += processedNodes * currentComponentSize;
48
49 // Update count of processed nodes
50 processedNodes += currentComponentSize;
51 }
52
53 return totalPairs;
54 }
55};
56
1/**
2 * Counts the number of pairs of nodes that are not connected in the graph
3 * @param n - The number of nodes in the graph
4 * @param edges - Array of edges where each edge is [nodeA, nodeB]
5 * @returns The count of unreachable pairs of nodes
6 */
7function countPairs(n: number, edges: number[][]): number {
8 // Build adjacency list representation of the graph
9 const adjacencyList: number[][] = Array.from({ length: n }, () => []);
10
11 // Add bidirectional edges to the adjacency list
12 for (const [nodeA, nodeB] of edges) {
13 adjacencyList[nodeA].push(nodeB);
14 adjacencyList[nodeB].push(nodeA);
15 }
16
17 // Track visited nodes during DFS traversal
18 const visited: boolean[] = Array(n).fill(false);
19
20 /**
21 * Performs depth-first search to count nodes in a connected component
22 * @param currentNode - The current node being visited
23 * @returns The number of nodes in the connected component
24 */
25 const dfs = (currentNode: number): number => {
26 // If node is already visited, return 0 to avoid double counting
27 if (visited[currentNode]) {
28 return 0;
29 }
30
31 // Mark current node as visited
32 visited[currentNode] = true;
33
34 // Start count with current node
35 let componentSize: number = 1;
36
37 // Recursively visit all neighbors and add their counts
38 for (const neighbor of adjacencyList[currentNode]) {
39 componentSize += dfs(neighbor);
40 }
41
42 return componentSize;
43 };
44
45 // Initialize result and cumulative sum of component sizes
46 let unreachablePairs: number = 0;
47 let cumulativeNodeCount: number = 0;
48
49 // Process each connected component
50 for (let node: number = 0; node < n; ++node) {
51 const componentSize: number = dfs(node);
52
53 // Add pairs between current component and all previously processed components
54 unreachablePairs += cumulativeNodeCount * componentSize;
55
56 // Update cumulative count with current component size
57 cumulativeNodeCount += componentSize;
58 }
59
60 return unreachablePairs;
61}
62
Time and Space Complexity
Time Complexity: O(n + m)
The algorithm performs a depth-first search (DFS) to identify connected components in the graph. Breaking down the operations:
- Building the adjacency list from edges takes
O(m)
time, where we iterate through allm
edges and add each edge twice (once for each direction). - The DFS traversal visits each node exactly once across all DFS calls, contributing
O(n)
time. - For each node visited, we explore all its adjacent edges. Since each edge is represented twice in the adjacency list (undirected graph), we traverse a total of
2m
edges across all DFS operations, which isO(m)
. - The calculation of pairs (
ans += s * t
ands += t
) is donen
times withO(1)
operations each.
Therefore, the total time complexity is O(n + m)
.
Space Complexity: O(n + m)
The space usage consists of:
- The adjacency list
g
stores all edges. Since it's an undirected graph, each edge is stored twice, requiringO(2m) = O(m)
space. - The visited array
vis
usesO(n)
space to track visited nodes. - The recursion stack for DFS can go up to
O(n)
depth in the worst case (e.g., a linear chain of nodes). - Other variables (
ans
,s
,t
,i
) useO(1)
space.
Therefore, the total space complexity is O(n + m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow in Large Graphs
Problem: When calculating nodes_processed * component_size
, the multiplication can overflow for large graphs. In the worst case with n=10^5 nodes split into two components, you could have approximately (50000 * 50000) = 2.5 * 10^9 pairs, which exceeds 32-bit integer limits in some languages.
Solution: In Python, this isn't an issue due to arbitrary precision integers, but in languages like Java or C++, use long/long long data types:
# Python handles this automatically, but in other languages: # Java: long totalPairs = 0L; # C++: long long totalPairs = 0;
2. Forgetting Bidirectional Edge Addition
Problem: Since the graph is undirected, each edge needs to be added in both directions. A common mistake is adding only one direction:
# WRONG - only adds one direction for node_a, node_b in edges: adjacency_list[node_a].append(node_b)
Solution: Always add both directions for undirected graphs:
# CORRECT - adds both directions for node_a, node_b in edges: adjacency_list[node_a].append(node_b) adjacency_list[node_b].append(node_a)
3. Stack Overflow with Deep Recursion
Problem: For graphs with very long paths (like a chain of n nodes), the recursive DFS can cause stack overflow. Python's default recursion limit is around 1000.
Solution: Either increase the recursion limit or use iterative DFS:
# Option 1: Increase recursion limit
import sys
sys.setrecursionlimit(10**6)
# Option 2: Iterative DFS
def dfs_iterative(start):
if visited[start]:
return 0
stack = [start]
component_size = 0
while stack:
node = stack.pop()
if visited[node]:
continue
visited[node] = True
component_size += 1
for neighbor in adjacency_list[node]:
if not visited[neighbor]:
stack.append(neighbor)
return component_size
4. Misunderstanding the Counting Logic
Problem: Attempting to count all pairs at once using combination formula C(n,2) - connected_pairs, or trying to count pairs within each component separately and summing them up.
Solution: Stick to the incremental approach where each new component's nodes form pairs with all previously processed nodes. This naturally avoids double-counting and correctly calculates cross-component pairs.
Which technique can we use to find the middle of a linked list?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!