Facebook Pixel

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:

  1. Building an adjacency list representation of the graph from the edges
  2. Using DFS to find each connected component and count its size t
  3. For each new component found, calculating the unreachable pairs as s * t, where s is the total number of nodes in all previously found components
  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Initialize a visited array vis of size n with all False values
  2. Initialize ans = 0 (total unreachable pairs) and s = 0 (cumulative nodes processed)
  3. For each node i from 0 to n-1:
    • Call dfs(i) to get the component size t
    • If t > 0 (meaning this is a new unvisited component):
      • Add s * t to ans (pairs between this component and all previous ones)
      • Add t to s (update the cumulative count)

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 (size t) and the other is from any previous component (total size s)
  • 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 Evaluator

Example 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, returns t = 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, returns t = 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, returns t = 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 all m 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 is O(m).
  • The calculation of pairs (ans += s * t and s += t) is done n times with O(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, requiring O(2m) = O(m) space.
  • The visited array vis uses O(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) use O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More