2685. Count the Number of Complete Components
Problem Description
You are tasked with finding the number of complete connected components in an undirected graph. The graph has n
vertices, each identified by a unique number from 0
to n - 1
. An array edges
contains pairs of integer indices representing the edges that connect vertices in the graph.
A connected component is a set of vertices in which each pair of vertices is connected by some path. Importantly, no vertex in a connected component connects to any vertex outside that component in the graph.
A complete connected component (also known as a clique) is a special kind of connected component where there is a direct edge between every pair of nodes within the component.
So, the objective is to count how many complete connected components there are in the given graph.
Flowchart Walkthrough
First, let's determine the appropriate algorithm using the Flowchart. Here's a step-by-step analysis:
Is it a graph?
- Yes: We need to consider if pairs of employees meet conditions (completeness) and form components which implies a graph where nodes represent employees, and edges represent the meet condition.
Is it a tree?
- No: The graph is likely not a tree as we're asked to count complete components where any node can have connections with multiple other nodes, making it general and potentially cyclical.
Is the problem related to directed acyclic graphs (DAGs)?
- No: The description does not suggest anything about directionality or acyclic conditions, as we're focused on completeness in components.
Is the problem related to shortest paths?
- No: The problem is centered on counting the number of complete components, not finding paths between nodes.
Does the problem involve connectivity?
- Yes: The problem is specifically about finding and counting complete components, which is directly related to how nodes are connected.
Is the graph weighted?
- No: All the information provided indicates that the conditions are binary (yes/no - meets/does not meet), not involving weights on the connections.
Conclusion: Based on the flowchart, DFS (Depth-First Search) is an apt choice for an unweighted connectivity problem to explore and count complete components in the graph, as it efficiently handles the visiting of all nodes in each connected component.
Intuition
To solve this problem, we can use Depth-First Search (DFS). DFS allows us to explore each vertex and its neighbors recursively. The solution employs DFS to count vertices (x
) and edges (y
) in each connected component. Since a complete graph with n
vertices should have exactly n * (n - 1) / 2
edges, this fact can be used to verify if a connected component is complete.
Here's how the algorithm works step by step:
- Create a graph representation using a list where each index represents a node and the values are lists of connected nodes.
- Initialize a list of boolean values to track visited nodes during DFS to ensure nodes are not counted more than once.
- Iterate through each node. If a node is unvisited, perform DFS starting from that node to determine the size of the connected component.
- During DFS, count the number of nodes (
x
) and the number of edges (y
). Note that since the graph is undirected, each edge will be counted twice - once for each endpoint. - After each DFS call, check if the number of edges matches the formula
x * (x - 1)
(which is doublex * (x - 1) / 2
since each edge is counted twice). If it does, then we have found a complete connected component. - Increment a counter for complete connected components any time the check in step 5 passes.
- After completing the iteration through all nodes, return the counter value.
This solution is efficient because it only requires a single pass through the graph, using DFS to explore the connected components and immediately checking which ones are complete.
Learn more about Depth-First Search, Breadth-First Search and Graph patterns.
Solution Approach
The solution uses the following algorithms, data structures, and patterns:
Depth-First Search (DFS): This algorithm allows us to explore all nodes in a connected component of the graph. We define a recursive dfs
function that takes a starting vertex i
and explores every connected vertex, returning the total count of vertices and edges in the connected component.
Adjacency List: The graph is represented as an adjacency list using a defaultdict(list)
from Python's collections library. This data structure is efficient for storing sparse graphs and allows us to quickly access all neighbors of a graph node.
Visited List: To keep track of which nodes have been visited during the DFS exploration, we maintain a list vis
of boolean values. this avoids counting the same node more than once and ensures we don't enter into an infinite loop by revisiting the same nodes.
Here's how the implementation follows the algorithm:
-
Initialize the graph as an adjacency list (
g
) from theedges
array, where for each edge[a, b]
we addb
to the list of adjacent vertices ofa
and vice versa since the graph is undirected. -
Create a visited list
vis
of boolean values, initialized toFalse
, to mark vertices as visited. -
For each vertex
i
from0
ton-1
, check if it has not been visited:- If not visited, call the
dfs
function on vertexi
. This will traverse all vertices in the same connected component asi
, marking them as visited and counting the vertices and edges. - The
dfs
function works as follows:- Mark the current vertex
i
as visited. - Initialize local variables
x
(vertex count) to1
andy
(edge count) to the length of the adjacency list ati
since every adjacent vertex represents an edge. - Iterate over each neighbor
j
of vertexi
. Ifj
is not visited, calldfs
recursively and updatex
andy
with the counts from the component connected throughj
. - Return the count of vertices
x
and the double count of edgesy
.
- Mark the current vertex
- If not visited, call the
-
After the recursive DFS, compare the number of vertices
a
and the double count of edgesb
using the formulaa * (a - 1)
. A complete component must have exactlya * (a - 1) / 2
edges, but since each edge is counted twice, we check fora * (a - 1)
without dividing by 2. If the component is complete, increment theans
. -
Finally, when all vertices are processed, return
ans
, which represents the count of complete connected components.
The solution effectively utilizes the dfs
function for graph traversal and the combinatorial property of complete graphs to detect complete connected components in a single scan.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's assume we have n = 5
vertices, and we are given the following edges
array, which defines an undirected graph:
edges = [[0, 1], [1, 2], [3, 4]]
The graph can be visualized as two separate connected components:
0 -- 1 -- 2 3 -- 4
Here's a step-by-step illustration of applying the solution approach:
- First, we create an adjacency list
g
for the given graph. This list will look like:
g = { 0: [1], 1: [0, 2], 2: [1], 3: [4], 4: [3] }
-
We initialize a visited list
vis = [False, False, False, False, False]
to keep track of visited nodes. -
We start iterating through each vertex. When iterating over vertex
0
, since it's not visited, we call thedfs
function. -
The
dfs
function now starts on0
, marking it as visited (vis[0] = True
) and settingx = 1
andy = len(g[0]) = 1
. -
Within DFS on vertex
0
, we move to its neighbor1
which is unvisited, and call DFS on1
, marking it visited and adding its own vertices and edges. We getx = 2
andy = 3
(since it's connected to 0 and 2). -
Continuing the DFS, we visit vertex
2
. No new vertices are discovered, but this adds to the edge count:y += len(g[2]) = 1
. -
The DFS call on vertex
0
ends, returningx = 3
andy = 4
. -
We then check if
x * (x - 1) == y
which would mean it's a complete connected component.3 * (3 - 1) = 6
which does not equal4
, so this component is not complete. -
We update the visited list for all vertices in the connected component containing
0
,1
, and2
. -
We continue the iteration to vertex
3
, calling DFS since it's not visited. -
Similar to the previous DFS, we mark
3
as visited, settingx = 1
andy = len(g[3]) = 1
since it's connected to4
. -
On visiting neighbor
4
, we updatex = 2
andy = 2
. -
After the DFS on
3
, we check ifx * (x - 1) == y
.2 * (2 - 1) = 2
which equalsy
, so this is indeed a complete connected component. -
We add
1
to our complete connected component countans
. -
Finally, after checking all vertices, we find that there is
1
complete connected component in the graph.
In conclusion, after traversing this example graph, our algorithm would correctly report that there is 1 complete connected component.
Solution Implementation
1from collections import defaultdict
2
3class Solution:
4 def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:
5 # dfs function to traverse the graph and return the number
6 # of nodes (vertex_count) and number of edges (edge_count) in the component.
7 def dfs(vertex: int) -> (int, int):
8 visited[vertex] = True
9 vertex_count, edge_count = 1, len(graph[vertex]) # Initialize counts with current node counts
10 for neighbor in graph[vertex]:
11 if not visited[neighbor]:
12 additional_vertices, additional_edges = dfs(neighbor)
13 vertex_count += additional_vertices
14 edge_count += additional_edges
15 return vertex_count, edge_count
16
17 # build the graph from the edges list
18 graph = defaultdict(list)
19 for a, b in edges:
20 graph[a].append(b)
21 graph[b].append(a)
22
23 visited = [False] * n # keep track of visited nodes
24 complete_components_count = 0 # counter for complete components
25
26 # check each node; if it's not visited, perform dfs from that node
27 for i in range(n):
28 if not visited[i]:
29 vertex_count, edge_count = dfs(i)
30 # In a complete graph AKA clique, the number of edges is vertex_count * (vertex_count - 1) / 2
31 # We multiply by 2 to compare with the undirected edge count (each edge counted twice)
32 if vertex_count * (vertex_count - 1) == edge_count:
33 complete_components_count += 1
34
35 return complete_components_count
36
1class Solution {
2 // Graph represented as an adjacency list and a visited array.
3 private List<Integer>[] graph;
4 private boolean[] visited;
5
6 // Method to count the number of complete components in the graph.
7 public int countCompleteComponents(int n, int[][] edges) {
8 // Initialize the graph with empty lists for each node.
9 graph = new List[n];
10 visited = new boolean[n];
11 Arrays.setAll(graph, k -> new ArrayList<>());
12
13 // Populate the adjacency list with the edges.
14 for (int[] edge : edges) {
15 int a = edge[0], b = edge[1];
16 graph[a].add(b);
17 graph[b].add(a);
18 }
19
20 int count = 0; // Counter for complete components.
21
22 // Traverse each node to check if it forms a complete component.
23 for (int i = 0; i < n; ++i) {
24 // If the node has not been visited, perform DFS.
25 if (!visited[i]) {
26 int[] result = dfs(i);
27 // A complete component has (n*(n-1))/2 edges. Here, result[0] is the number of nodes (n)
28 // and result[1] is the number of edges in this component.
29 if (result[0] * (result[0] - 1) == result[1]) {
30 count++;
31 }
32 }
33 }
34 return count; // Return the total count of complete components.
35 }
36
37 // Depth First Search (DFS) helper method to compute the total number of nodes and edges in a component.
38 private int[] dfs(int index) {
39 visited[index] = true;
40 int nodesCount = 1; // Start with one node, the one we are currently at.
41 int edgesCount = graph[index].size(); // Counts edges connected to the current node.
42
43 // Recursively visit all the neighbors that have not been visited.
44 for (int neighbor : graph[index]) {
45 if (!visited[neighbor]) {
46 int[] result = dfs(neighbor);
47 // Increment the count of nodes and edges with the counts from the neighbor.
48 nodesCount += result[0];
49 edgesCount += result[1];
50 }
51 }
52
53 // Return the total count of nodes and edges in this component.
54 return new int[] {nodesCount, edgesCount};
55 }
56}
57
1#include <vector>
2#include <cstring>
3#include <functional>
4
5using namespace std;
6
7class Solution {
8public:
9 int countCompleteComponents(int n, vector<vector<int>>& edges) {
10 vector<vector<int>> graph(n); // Using 'graph' for clarity.
11 vector<bool> visited(n, false);// Using vector of bool for visited to replace the C-style array.
12
13 // Building the undirected graph
14 for (auto& edge : edges) {
15 int a = edge[0], b = edge[1];
16 graph[a].push_back(b);
17 graph[b].push_back(a);
18 }
19
20 // Declaration of Depth-First Search (DFS) lambda function
21 function<pair<int, int>(int)> dfs = [&](int node) -> pair<int, int> {
22 visited[node] = true;
23 int verticesCount = 1; // 'x' has been replaced with 'verticesCount'
24 int edgesCount = graph[node].size(); // 'y' has been replaced with 'edgesCount'
25
26 // Recursive DFS calls
27 for (int neighbor : graph[node]) {
28 if (!visited[neighbor]) {
29 auto [subtreeVertices, subtreeEdges] = dfs(neighbor);
30 verticesCount += subtreeVertices;
31 edgesCount += subtreeEdges;
32 }
33 }
34 return make_pair(verticesCount, edgesCount);
35 };
36
37 int completeComponents = 0; // Using 'completeComponents' instead of 'ans' for clarity
38
39 // Checking each connected component of the graph
40 for (int i = 0; i < n; ++i) {
41 if (!visited[i]) {
42 auto [componentVertices, componentEdges] = dfs(i);
43 // Check if the connected component is a complete graph
44 // A complete graph with 'n' vertices has 'n*(n-1)/2' edges
45 if (componentVertices * (componentVertices - 1) == componentEdges) {
46 completeComponents++;
47 }
48 }
49 }
50 return completeComponents; // Return the count of complete components
51 }
52};
53
1type Graph = number[][];
2let graph: Graph;
3let visited: boolean[];
4
5// Builds the undirected graph from the edges
6const buildGraph = (n: number, edges: number[][]): void => {
7 graph = Array.from({ length: n }, () => []);
8 visited = new Array(n).fill(false);
9
10 edges.forEach(edge => {
11 const [a, b] = edge;
12 graph[a].push(b);
13 graph[b].push(a);
14 });
15};
16
17// Defines a Depth-First Search (DFS) function to explore the graph
18const depthFirstSearch = (node: number): [number, number] => {
19 visited[node] = true;
20 let verticesCount = 1;
21 let edgesCount = graph[node].length;
22
23 graph[node].forEach(neighbor => {
24 if (!visited[neighbor]) {
25 const [subtreeVertices, subtreeEdges] = depthFirstSearch(neighbor);
26 verticesCount += subtreeVertices;
27 edgesCount += subtreeEdges;
28 }
29 });
30
31 return [verticesCount, edgesCount];
32};
33
34// Counts the number of complete components in the graph
35const countCompleteComponents = (n: number, edges: number[][]): number => {
36 buildGraph(n, edges);
37 let completeComponents = 0;
38
39 for (let i = 0; i < n; i++) {
40 if (!visited[i]) {
41 const [componentVertices, componentEdges] = depthFirstSearch(i);
42
43 // Checks if the connected component is a complete graph
44 // A complete graph with 'n' vertices has 'n*(n-1)/2' edges
45 if (componentVertices * (componentVertices - 1) / 2 === componentEdges) {
46 completeComponents++;
47 }
48 }
49 }
50
51 return completeComponents;
52};
53
Time and Space Complexity
Time Complexity
The time complexity of the provided code is O(N + E), where N
represents the number of nodes and E
represents the number of edges in the graph. The reasoning behind this assessment is as follows:
-
Iterating over all edges to create the graph
g
yields O(E), as it is directly proportional to the number of edges. -
The
dfs
function visits each node exactly once due to the guard conditionif not vis[i]:
, which prevents revisiting. Each call todfs
also iterates over all neighbors of the node, which results in a total of O(E) for alldfs
calls across all nodes because each edge will be considered exactly twice (once from each incident node). -
The
for
loop overn
nodes is O(N), as it will attempt adfs
from each unvisited node.
The combination of creating the adjacency list and the DFS traversal then constitutes O(N + E).
Space Complexity
The space complexity of the code is O(N + E) as well. This assessment considers the following components that utilize memory:
-
The adjacency list
g
can potentially hold all the edges, which has a space requirement of O(E). -
The visited list
vis
has a space requirement of O(N), since it stores a boolean for each node. -
The recursion stack for DFS can also grow up to O(N) in the case of a deep or unbalanced tree.
-
The
edges
list itself occupies O(E).
Taking all these factors into account, the upper bound on space utilized by the algorithm is O(N + E), which accounts for all nodes and edges stored and processed.
Learn more about how to find time and space complexity quickly using problem constraints.
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
https algomonster s3 us east 2 amazonaws com 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
https algomonster s3 us east 2 amazonaws com 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
https algomonster s3 us east 2 amazonaws com cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could
Want a Structured Path to Master System Design Too? Don’t Miss This!