Facebook Pixel

2685. Count the Number of Complete Components

Problem Description

You have an undirected graph with n vertices labeled from 0 to n - 1. The graph's edges are given as a 2D array edges, where each edges[i] = [ai, bi] represents an undirected edge between vertices ai and bi.

Your task is to count how many complete connected components exist in this graph.

A connected component is a group of vertices where:

  • You can reach any vertex from any other vertex within the group by following edges
  • No vertex in the group has an edge to any vertex outside the group

A connected component is complete when every vertex in the component is directly connected to every other vertex in that component by an edge. In other words, if a component has k vertices, it must have exactly k * (k - 1) / 2 edges to be complete (since every pair of vertices needs an edge between them).

For example:

  • If you have 3 vertices that are all connected to each other (forming a triangle), that's a complete component
  • If you have 3 vertices where only 2 edges exist (forming a line), that's NOT a complete component
  • A single isolated vertex (with no edges) counts as a complete component

The solution uses DFS to identify each connected component, counting both the number of vertices and total degree (sum of edges) in each component. For a complete component with a vertices, the total degree sum should equal a * (a - 1) (since each vertex connects to all other a - 1 vertices, and each edge is counted twice in the degree sum).

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 states we have an undirected graph with n vertices and edges connecting them.

Is it a tree?

  • No: The graph is not necessarily a tree. It can have multiple disconnected components, and each component might have cycles (especially since we're looking for complete components where every vertex connects to every other vertex).

Is the problem related to directed acyclic graphs?

  • No: The graph is undirected, not directed, and we're not dealing with DAG-specific properties.

Is the problem related to shortest paths?

  • No: We're not finding shortest paths between vertices. We're identifying and counting complete connected components.

Does the problem involve connectivity?

  • Yes: The core of the problem is about finding connected components and determining if they are complete (fully connected within themselves).

Does the problem have small constraints?

  • The constraints aren't explicitly stated in the problem, but for connectivity problems, we often need to explore all vertices in each component. DFS is a natural choice for exploring connected components.

Conclusion: The flowchart leads us to use DFS for this problem. DFS is perfect for:

  1. Traversing each connected component completely
  2. Counting the number of vertices in each component
  3. Counting the total degree (number of edges) in each component
  4. Determining if a component is complete by checking if the number of edges matches what's expected for a complete graph

The DFS approach allows us to visit each vertex once, mark it as visited, and gather the necessary information (vertex count and edge count) to determine if each component is complete.

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

Intuition

The key insight is recognizing what makes a connected component "complete". In a complete graph, every vertex must be connected to every other vertex. If we have k vertices in a complete component, then each vertex must have exactly k-1 edges (connecting to all other vertices).

This gives us a mathematical property to check: in a complete component with k vertices, there should be exactly k * (k-1) / 2 unique edges. But since we're working with an undirected graph where each edge is counted from both endpoints, when we sum up the degrees of all vertices in the component, we get k * (k-1) (each vertex has degree k-1, and we have k vertices).

So our strategy becomes:

  1. Use DFS to explore each connected component
  2. While exploring, count two things:
    • The number of vertices in the component
    • The sum of degrees (total number of edge endpoints) in the component
  3. A component is complete if and only if: vertex_count * (vertex_count - 1) == total_degree_sum

Why does this work? Consider a triangle (3 vertices, all connected):

  • Each vertex connects to 2 others, so each has degree 2
  • Total degree sum = 3 Γ— 2 = 6
  • Expected for complete: 3 Γ— (3-1) = 6 βœ“

For an incomplete line of 3 vertices (A-B-C):

  • A has degree 1, B has degree 2, C has degree 1
  • Total degree sum = 4
  • Expected for complete: 3 Γ— 2 = 6 βœ—

The DFS naturally handles disconnected components by starting fresh traversals from unvisited vertices, allowing us to check each component independently for completeness.

Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.

Solution Approach

The implementation uses DFS with an adjacency list representation to efficiently explore and analyze each connected component.

Step 1: Build the Graph First, we construct an adjacency list using a defaultdict(list) to represent the undirected graph. For each edge [a, b], we add b to a's neighbor list and a to b's neighbor list:

g = defaultdict(list)
for a, b in edges:
    g[a].append(b)
    g[b].append(a)

Step 2: Initialize Tracking Variables We maintain a vis array of size n to track visited vertices, initially all set to False:

vis = [False] * n

Step 3: Define the DFS Function The DFS function returns two values for each component:

  • x: the number of vertices in the component
  • y: the sum of degrees in the component
def dfs(i: int) -> (int, int):
    vis[i] = True
    x, y = 1, len(g[i])  # Start with current vertex and its degree
    for j in g[i]:
        if not vis[j]:
            a, b = dfs(j)
            x += a  # Add vertices from subtree
            y += b  # Add degrees from subtree
    return x, y

The function marks vertex i as visited, initializes the count with 1 vertex (itself) and its degree len(g[i]), then recursively explores unvisited neighbors, accumulating their vertex and degree counts.

Step 4: Process All Components We iterate through all vertices, starting a new DFS for each unvisited vertex (which represents a new component):

ans = 0
for i in range(n):
    if not vis[i]:
        a, b = dfs(i)
        ans += a * (a - 1) == b

For each component found, we check if it's complete using the formula a * (a - 1) == b, where:

  • a is the number of vertices
  • b is the total degree sum
  • The expression a * (a - 1) == b returns True (1) if complete, False (0) otherwise

The boolean result is implicitly converted to 0 or 1 and added to our answer counter.

Time Complexity: O(V + E) where V is the number of vertices and E is the number of edges, as we visit each vertex and edge once.

Space Complexity: O(V + E) for the adjacency list and O(V) for the 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 concrete example with n = 6 vertices and edges = [[0,1], [0,2], [1,2], [3,4]].

Step 1: Build the Graph

First, we construct the adjacency list:

g[0] = [1, 2]
g[1] = [0, 2]
g[2] = [0, 1]
g[3] = [4]
g[4] = [3]
g[5] = []

We also initialize vis = [False, False, False, False, False, False].

Step 2: Process Component 1 (starting from vertex 0)

Start DFS from vertex 0:

  • Mark vis[0] = True
  • Initialize x = 1 (vertex 0), y = 2 (degree of vertex 0)
  • Explore neighbor 1:
    • Mark vis[1] = True
    • At vertex 1: x = 1, y = 2 (degree of vertex 1)
    • Explore neighbor 2 from vertex 1:
      • Mark vis[2] = True
      • At vertex 2: x = 1, y = 2 (degree of vertex 2)
      • No unvisited neighbors, return (1, 2)
    • Back at vertex 1: accumulate to get x = 2, y = 4
    • Return (2, 4)
  • Back at vertex 0: accumulate to get x = 3, y = 6

Component 1 has 3 vertices and total degree 6. Check: 3 * (3-1) = 6, which equals the degree sum. This is a complete component (triangle).

Step 3: Process Component 2 (starting from vertex 3)

Since vertices 0, 1, 2 are visited, we continue to vertex 3:

  • Mark vis[3] = True
  • Initialize x = 1, y = 1 (degree of vertex 3)
  • Explore neighbor 4:
    • Mark vis[4] = True
    • At vertex 4: x = 1, y = 1 (degree of vertex 4)
    • No unvisited neighbors, return (1, 1)
  • Back at vertex 3: accumulate to get x = 2, y = 2

Component 2 has 2 vertices and total degree 2. Check: 2 * (2-1) = 2, which equals the degree sum. This is a complete component (single edge).

Step 4: Process Component 3 (vertex 5)

Vertex 5 is unvisited and isolated:

  • Mark vis[5] = True
  • Initialize x = 1, y = 0 (degree of vertex 5)
  • No neighbors to explore
  • Return (1, 0)

Component 3 has 1 vertex and total degree 0. Check: 1 * (1-1) = 0, which equals the degree sum. This is a complete component (isolated vertex).

Final Answer: 3 complete connected components

Each component satisfied the completeness condition:

  • Triangle (0-1-2): vertices=3, degree_sum=6, check: 3Γ—2=6 βœ“
  • Edge (3-4): vertices=2, degree_sum=2, check: 2Γ—1=2 βœ“
  • Isolated (5): vertices=1, degree_sum=0, check: 1Γ—0=0 βœ“

Solution Implementation

1from collections import defaultdict
2from typing import List
3
4class Solution:
5    def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:
6        """
7        Count the number of complete connected components in an undirected graph.
8        A complete component is one where every pair of vertices is connected by an edge.
9      
10        Args:
11            n: Number of vertices in the graph (0 to n-1)
12            edges: List of edges where each edge is [u, v]
13      
14        Returns:
15            Number of complete connected components
16        """
17      
18        def dfs(node: int) -> tuple[int, int]:
19            """
20            Perform DFS to explore a connected component.
21          
22            Args:
23                node: Current node to explore
24          
25            Returns:
26                Tuple of (vertex_count, edge_count) in this component
27            """
28            # Mark current node as visited
29            visited[node] = True
30          
31            # Initialize counts: 1 vertex (current), degree of current node
32            vertex_count = 1
33            edge_count = len(adjacency_list[node])
34          
35            # Explore all neighbors
36            for neighbor in adjacency_list[node]:
37                if not visited[neighbor]:
38                    # Recursively explore unvisited neighbors
39                    vertices, edges = dfs(neighbor)
40                    vertex_count += vertices
41                    edge_count += edges
42          
43            return vertex_count, edge_count
44      
45        # Build adjacency list representation of the graph
46        adjacency_list = defaultdict(list)
47        for u, v in edges:
48            adjacency_list[u].append(v)
49            adjacency_list[v].append(u)
50      
51        # Initialize visited array to track explored vertices
52        visited = [False] * n
53      
54        # Count complete components
55        complete_component_count = 0
56      
57        # Process each connected component
58        for vertex in range(n):
59            if not visited[vertex]:
60                # Get vertex and edge counts for this component
61                vertices, edges = dfs(vertex)
62              
63                # Check if component is complete
64                # In a complete graph with v vertices: edges = v * (v - 1)
65                # Since each edge is counted twice in adjacency list
66                if vertices * (vertices - 1) == edges:
67                    complete_component_count += 1
68      
69        return complete_component_count
70
1class Solution {
2    // Adjacency list representation of the graph
3    private List<Integer>[] adjacencyList;
4    // Visited array to track which nodes have been processed
5    private boolean[] visited;
6
7    public int countCompleteComponents(int n, int[][] edges) {
8        // Initialize the adjacency list and visited array
9        adjacencyList = new List[n];
10        visited = new boolean[n];
11      
12        // Create an empty list for each node
13        Arrays.setAll(adjacencyList, index -> new ArrayList<>());
14      
15        // Build the undirected graph by adding edges in both directions
16        for (int[] edge : edges) {
17            int nodeA = edge[0];
18            int nodeB = edge[1];
19            adjacencyList[nodeA].add(nodeB);
20            adjacencyList[nodeB].add(nodeA);
21        }
22      
23        int completeComponentCount = 0;
24      
25        // Iterate through all nodes to find connected components
26        for (int node = 0; node < n; ++node) {
27            if (!visited[node]) {
28                // Perform DFS to get component statistics
29                int[] componentStats = dfs(node);
30                int nodeCount = componentStats[0];
31                int edgeCount = componentStats[1];
32              
33                // Check if this component is complete
34                // In a complete graph with n nodes, there are n*(n-1) directed edges
35                // (or n*(n-1)/2 undirected edges, but we count each edge twice)
36                if (nodeCount * (nodeCount - 1) == edgeCount) {
37                    ++completeComponentCount;
38                }
39            }
40        }
41      
42        return completeComponentCount;
43    }
44
45    /**
46     * Performs DFS to explore a connected component
47     * @param currentNode The starting node for DFS
48     * @return An array where [0] is the number of nodes and [1] is the total degree sum
49     */
50    private int[] dfs(int currentNode) {
51        // Mark the current node as visited
52        visited[currentNode] = true;
53      
54        // Initialize counters: nodeCount starts at 1 (current node)
55        // degreeSum starts with the degree of current node
56        int nodeCount = 1;
57        int degreeSum = adjacencyList[currentNode].size();
58      
59        // Explore all neighbors
60        for (int neighbor : adjacencyList[currentNode]) {
61            if (!visited[neighbor]) {
62                // Recursively explore unvisited neighbors
63                int[] neighborStats = dfs(neighbor);
64                nodeCount += neighborStats[0];
65                degreeSum += neighborStats[1];
66            }
67        }
68      
69        // Return the accumulated statistics for this component
70        return new int[] {nodeCount, degreeSum};
71    }
72}
73
1class Solution {
2public:
3    int countCompleteComponents(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 vertex1 = edge[0];
8            int vertex2 = edge[1];
9            adjacencyList[vertex1].push_back(vertex2);
10            adjacencyList[vertex2].push_back(vertex1);
11        }
12      
13        // Track visited vertices during DFS traversal
14        vector<bool> visited(n, false);
15      
16        // DFS function to explore a connected component
17        // Returns: pair<vertex_count, edge_count> in the component
18        function<pair<int, int>(int)> dfs = [&](int currentVertex) -> pair<int, int> {
19            visited[currentVertex] = true;
20          
21            // Initialize counts: 1 vertex (current), degree edges from current vertex
22            int vertexCount = 1;
23            int totalEdgeCount = adjacencyList[currentVertex].size();
24          
25            // Explore all neighbors
26            for (int neighbor : adjacencyList[currentVertex]) {
27                if (!visited[neighbor]) {
28                    auto [neighborVertices, neighborEdges] = dfs(neighbor);
29                    vertexCount += neighborVertices;
30                    totalEdgeCount += neighborEdges;
31                }
32            }
33          
34            return make_pair(vertexCount, totalEdgeCount);
35        };
36      
37        int completeComponentCount = 0;
38      
39        // Process each connected component
40        for (int vertex = 0; vertex < n; ++vertex) {
41            if (!visited[vertex]) {
42                auto [componentVertices, componentEdges] = dfs(vertex);
43              
44                // Check if component is complete
45                // In a complete graph with v vertices, there are v*(v-1)/2 edges
46                // Since we count each edge twice (from both endpoints), we have v*(v-1) total
47                if (componentVertices * (componentVertices - 1) == componentEdges) {
48                    ++completeComponentCount;
49                }
50            }
51        }
52      
53        return completeComponentCount;
54    }
55};
56
1function countCompleteComponents(n: number, edges: number[][]): number {
2    // Build adjacency list representation of the graph
3    const adjacencyList: number[][] = Array(n).fill(null).map(() => []);
4  
5    for (const edge of edges) {
6        const vertex1 = edge[0];
7        const vertex2 = edge[1];
8        adjacencyList[vertex1].push(vertex2);
9        adjacencyList[vertex2].push(vertex1);
10    }
11  
12    // Track visited vertices during DFS traversal
13    const visited: boolean[] = Array(n).fill(false);
14  
15    // DFS function to explore a connected component
16    // Returns: [vertexCount, edgeCount] in the component
17    const dfs = (currentVertex: number): [number, number] => {
18        visited[currentVertex] = true;
19      
20        // Initialize counts: 1 vertex (current), degree edges from current vertex
21        let vertexCount = 1;
22        let totalEdgeCount = adjacencyList[currentVertex].length;
23      
24        // Explore all neighbors
25        for (const neighbor of adjacencyList[currentVertex]) {
26            if (!visited[neighbor]) {
27                const [neighborVertices, neighborEdges] = dfs(neighbor);
28                vertexCount += neighborVertices;
29                totalEdgeCount += neighborEdges;
30            }
31        }
32      
33        return [vertexCount, totalEdgeCount];
34    };
35  
36    let completeComponentCount = 0;
37  
38    // Process each connected component
39    for (let vertex = 0; vertex < n; vertex++) {
40        if (!visited[vertex]) {
41            const [componentVertices, componentEdges] = dfs(vertex);
42          
43            // Check if component is complete
44            // In a complete graph with v vertices, there are v*(v-1)/2 edges
45            // Since we count each edge twice (from both endpoints), we have v*(v-1) total
46            if (componentVertices * (componentVertices - 1) === componentEdges) {
47                completeComponentCount++;
48            }
49        }
50    }
51  
52    return completeComponentCount;
53}
54

Time and Space Complexity

Time Complexity: O(V + E) where V is the number of vertices (n) and E is the number of edges.

  • Building the adjacency list takes O(E) time as we iterate through all edges once.
  • The DFS traversal visits each vertex exactly once, taking O(V) time for vertex visits.
  • For each vertex, we iterate through its adjacency list. Since each edge is represented twice in the adjacency list (once for each endpoint), the total time spent iterating through adjacencies is O(2E) = O(E).
  • The check a * (a - 1) == b for each component takes O(1) time.
  • Overall: O(V + E)

Space Complexity: O(V + E)

  • The adjacency list g stores all edges, requiring O(E) space (each edge appears twice).
  • The visited array vis requires O(V) space.
  • The recursion stack for DFS can go up to O(V) depth in the worst case (when the graph is a path).
  • Overall: O(V + E)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Forgetting to Account for Double-Counting of Edges

The Pitfall: When building an undirected graph using an adjacency list, each edge is added twice (once for each direction). This means when we sum up the degrees of all vertices in a component, we're counting each edge twice. A common mistake is to compare this sum directly with k * (k - 1) / 2 (the number of edges in a complete graph) instead of k * (k - 1) (the doubled count).

Example of Incorrect Code:

# WRONG: Comparing with single edge count
if vertices * (vertices - 1) // 2 == edges:  # This would always fail!
    complete_component_count += 1

The Solution: Remember that in the adjacency list representation, the sum of degrees equals twice the number of edges. So for a complete component with k vertices:

  • Number of actual edges: k * (k - 1) / 2
  • Sum of degrees (what we calculate): k * (k - 1)

Correct Code:

# CORRECT: Account for double-counting
if vertices * (vertices - 1) == edges:
    complete_component_count += 1

2. Not Handling Isolated Vertices (Zero-Degree Nodes)

The Pitfall: If a vertex has no edges at all, it won't appear in the adjacency list when using a regular dictionary. This could lead to either missing these vertices entirely or getting a KeyError when trying to access them.

Example of Problematic Code:

# Using regular dict might miss isolated vertices
adjacency_list = {}
for u, v in edges:
    if u not in adjacency_list:
        adjacency_list[u] = []
    if v not in adjacency_list:
        adjacency_list[v] = []
    adjacency_list[u].append(v)
    adjacency_list[v].append(u)

# Later in DFS:
edge_count = len(adjacency_list[node])  # KeyError if node is isolated!

The Solution: Use defaultdict(list) which automatically creates an empty list for any new key access, ensuring isolated vertices return a degree of 0:

Correct Code:

from collections import defaultdict

adjacency_list = defaultdict(list)
for u, v in edges:
    adjacency_list[u].append(v)
    adjacency_list[v].append(u)

# Now len(adjacency_list[isolated_node]) returns 0 safely

3. Integer Division Instead of Exact Comparison

The Pitfall: Using integer division (//) when checking the complete component condition can lead to incorrect results due to rounding.

Example of Incorrect Code:

# WRONG: Integer division loses precision
expected_edges = vertices * (vertices - 1) // 2
actual_edges = edges // 2  # Dividing the double-counted sum
if expected_edges == actual_edges:
    complete_component_count += 1

The Solution: Keep the comparison in terms of the double-counted values to avoid any division:

Correct Code:

# CORRECT: Compare without division
if vertices * (vertices - 1) == edges:
    complete_component_count += 1

This approach is both more efficient (no division operation) and more accurate (no potential for rounding errors).

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

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

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

Load More