Problem of The Day: Count the Number of Complete Components
Problem StatementPermalink
IntuitionPermalink
This algorithm aims to determine the number of complete connected components in an undirected graph. A complete component is a connected subgraph where every node is directly connected to every other node within the component.
ApproachPermalink
- Union-Find Data Structure
- Initialize a Union-Find (Disjoint Set Union) data structure to efficiently manage connected components.
- The
find
method implements path compression for efficient lookups. - The
union
method connects two nodes, merging their components while maintaining rank to optimize merging.
-
Building Components
- Traverse the given edge list and use the
union
function to group connected nodes. - After processing all edges, apply
find
on each node to ensure all connections are properly linked. - Use a dictionary to store nodes grouped by their root representatives.
- Traverse the given edge list and use the
- Checking Completeness
- Iterate over each component and verify if all possible edges exist between its nodes.
- If any expected edge is missing, the component is not complete.
- Count only the complete components.
ComplexityPermalink
-
Time Complexity:
- Union-Find operations: The
find
andunion
operations run in nearly constant time, i.e., O(α(n)), where α is the inverse Ackermann function. - Edge processing: O(E), where E is the number of edges.
- Completeness check: In the worst case, O(V²) where V is the number of nodes in a component.
- Overall, the worst-case complexity is O(V² + E).
- Union-Find operations: The
-
Space Complexity:
- O(V) for storing parent/root and rank arrays.
- O(V) for the components dictionary.
- Total space complexity is O(V + E).
CodePermalink
from collections import defaultdict
from typing import List
class UnionFind:
def __init__(self, n):
self.root = [i for i in range(n)]
self.rank = [0] * n
self.num_of_components = n
def find(self, x):
if self.root[x] == x:
return x
self.root[x] = self.find(self.root[x]) # Path compression
return self.root[x]
def union(self, x, y):
rootX = self.find(x)
rootY = self.find(y)
if rootX == rootY:
return
if self.rank[rootX] < self.rank[rootY]:
self.root[rootX] = rootY
elif self.rank[rootX] > self.rank[rootY]:
self.root[rootY] = rootX
else:
self.root[rootY] = rootX
self.rank[rootX] += 1
self.num_of_components -= 1
class Solution:
def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:
if not edges:
return n
uf = UnionFind(n)
components = defaultdict(list)
for x, y in edges:
uf.union(x, y)
for i in range(n):
uf.find(i) # Ensure proper root assignment
for i, root in enumerate(uf.root):
components[root].append(i)
res = uf.num_of_components
for root, nodes in components.items():
n = len(nodes)
is_completed = True
for i in range(n):
p = nodes[i]
for j in range(i + 1, n):
q = nodes[j]
if [p, q] not in edges and [q, p] not in edges:
is_completed = False
break
if not is_completed:
res -= 1
return max(res, 0)
EditorialPermalink
Approach 1: Adjacency ListPermalink
class Solution:
def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:
# Adjacency lists for each vertex
graph = [[] for _ in range(n)]
# Map to store frequency of each unique adjacency list
component_freq = defaultdict(int)
# Initialize adjacency lists with self-loops
for vertex in range(n):
graph[vertex] = [vertex]
# Build adjacency lists from edges
for v1, v2 in edges:
graph[v1].append(v2)
graph[v2].append(v1)
# Count frequency of each unique adjacency pattern
for vertex in range(n):
neighbors = tuple(sorted(graph[vertex]))
component_freq[neighbors] += 1
# Count complete components where size equals frequency
return sum(
1
for neighbors, freq in component_freq.items()
if len(neighbors) == freq
)
Approach 2: Depth-First Search (DFS)Permalink
class Solution:
def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:
# Adjacency lists for each vertex
graph = defaultdict(list)
# Build adjacency lists from edges
for v1, v2 in edges:
graph[v1].append(v2)
graph[v2].append(v1)
complete_count = 0
visited = set()
def _dfs(curr: int, info: list) -> None:
visited.add(curr)
info[0] += 1 # Increment vertex count
info[1] += len(graph[curr]) # Add edges from current vertex
# Explore unvisited neighbors
for next_vertex in graph[curr]:
if next_vertex not in visited:
_dfs(next_vertex, info)
# Process each unvisited vertex
for vertex in range(n):
if vertex in visited:
continue
# info[0] = vertices count, info[1] = total edges count
component_info = [0, 0]
_dfs(vertex, component_info)
# Check if component is complete - edges should be vertices * (vertices-1)
if component_info[0] * (component_info[0] - 1) == component_info[1]:
complete_count += 1
return complete_count
Approach 3: Breadth-First Search (BFS)Permalink
class Solution:
def countCompleteComponents(self, n: int, edges: list[list[int]]) -> int:
# Create adjacency list representation of the graph
graph = [[] for _ in range(n)]
# Build graph from edges
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = [False] * n
complete_components = 0
# Process each unvisited vertex
for vertex in range(n):
if not visited[vertex]:
# BFS to find all vertices in the current component
component = []
queue = [vertex]
visited[vertex] = True
while queue:
current = queue.pop(0)
component.append(current)
# Process neighbors
for neighbor in graph[current]:
if not visited[neighbor]:
queue.append(neighbor)
visited[neighbor] = True
# Check if component is complete (all vertices have the right number of edges)
is_complete = True
for node in component:
if len(graph[node]) != len(component) - 1:
is_complete = False
break
if is_complete:
complete_components += 1
return complete_components
Approach 4: Disjoint Set Union (Union-Find)Permalink
class UnionFind:
def __init__(self, n):
self.parent = [-1] * n
self.size = [1] * n
def _find(self, node):
# Find root of component with path compression
if self.parent[node] == -1:
return node
self.parent[node] = self._find(self.parent[node])
return self.parent[node]
def _union(self, node_1, node_2):
# Union by size
root_1 = self._find(node_1)
root_2 = self._find(node_2)
if root_1 == root_2:
return
# Merge smaller component into larger one
if self.size[root_1] > self.size[root_2]:
self.parent[root_2] = root_1
self.size[root_1] += self.size[root_2]
else:
self.parent[root_1] = root_2
self.size[root_2] += self.size[root_1]
class Solution:
def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:
# Initialize Union Find and edge counter
dsu = UnionFind(n)
edge_count = {}
# Connect components using edges
for edge in edges:
dsu._union(edge[0], edge[1])
# Count edges in each component
for edge in edges:
root = dsu._find(edge[0])
edge_count[root] = edge_count.get(root, 0) + 1
# Check if each component is complete
complete_count = 0
for vertex in range(n):
if dsu._find(vertex) == vertex: # If vertex is root
node_count = dsu.size[vertex]
expected_edges = (node_count * (node_count - 1)) // 2
if edge_count.get(vertex, 0) == expected_edges:
complete_count += 1
return complete_count