952. Largest Component Size by Common Factor
Problem Description
You have an array of unique positive integers called nums
. Your task is to build a graph based on these numbers and find the largest connected component in that graph.
The graph is constructed with the following rules:
- Each number in the array becomes a node in the graph (so if you have
n
numbers, you haven
nodes) - The nodes are labeled with the actual values from the array (not indices)
- Two nodes
nums[i]
andnums[j]
are connected by an edge if and only if they share a common factor greater than 1
A common factor greater than 1 means the two numbers have at least one common divisor that is greater than 1. For example:
- 6 and 9 share common factor 3 (both are divisible by 3)
- 4 and 6 share common factor 2 (both are divisible by 2)
- 5 and 7 don't share any common factor greater than 1
A connected component is a group of nodes where you can reach any node from any other node in the group by following edges. Your goal is to find the size (number of nodes) of the largest connected component in this graph.
For example, if nums = [4, 6, 15, 35]
:
- 4 and 6 are connected (common factor: 2)
- 15 and 35 are connected (common factor: 5)
- But there's no connection between the group {4, 6} and {15, 35}
- So you have two connected components of size 2 each, and the answer would be 2
Intuition
The key insight is that if two numbers share a common factor, they belong to the same connected component. But instead of checking every pair of numbers directly (which would be expensive), we can think about this problem differently.
Consider that if two numbers share a common factor f
, then both numbers are connected to f
. This means we can use the factors themselves as "bridges" to connect numbers. For example, if we have numbers 6 and 9, instead of directly connecting them, we can say:
- 6 is connected to its factor 3
- 9 is connected to its factor 3
- Therefore, 6 and 9 are in the same component through their shared factor 3
This naturally leads us to use Union-Find (Disjoint Set Union), where we can union each number with all of its factors. The beauty of this approach is that if multiple numbers share the same factor, they'll all get connected through that factor automatically.
For each number v
, we only need to find its factors up to √v
because factors come in pairs. If i
divides v
, then v/i
also divides v
. So when we find factor i
, we union v
with both i
and v/i
.
After processing all numbers and their factors, numbers that share any common factors will end up in the same connected component (they'll have the same root in the Union-Find structure). We then count how many numbers belong to each root and return the maximum count.
The trick of using factors as intermediary nodes transforms the problem from "check if two numbers share factors" to "connect numbers to their factors", which is much more efficient and elegant.
Learn more about Union Find and Math patterns.
Solution Approach
The solution uses the Union-Find (Disjoint Set Union) data structure to efficiently group numbers that share common factors.
Union-Find Implementation:
The UnionFind
class maintains a parent array p
where p[x]
represents the parent of element x
. Initially, each element is its own parent.
find(x)
: Returns the root of the set containingx
. It uses path compression (self.p[x] = self.find(self.p[x])
) to optimize future lookups by making every node point directly to the root.union(a, b)
: Merges the sets containinga
andb
by making one root point to the other.
Main Algorithm:
-
Initialize Union-Find: Create a Union-Find structure with size
max(nums) + 1
to accommodate all possible values and their factors. -
Factor Finding and Union Operations: For each number
v
in the array:- Start checking potential factors from
i = 2
- Continue while
i <= √v
(written asi <= v // i
to avoid floating-point operations) - If
i
dividesv
evenly (v % i == 0
):- Union
v
with factori
- Union
v
with its complementary factorv // i
- Union
- Increment
i
and continue
- Start checking potential factors from
-
Count Component Sizes: After all unions are complete:
- For each number in the original array, find its root using
uf.find(v)
- Count how many numbers belong to each root using
Counter
- Return the maximum count, which represents the largest connected component
- For each number in the original array, find its root using
Example Walkthrough:
For nums = [6, 8, 12]
:
- Process 6: factors are 2 and 3, union(6,2), union(6,3)
- Process 8: factors are 2 and 4, union(8,2), union(8,4)
- Process 12: factors are 2,6 and 3,4, union(12,2), union(12,6), union(12,3), union(12,4)
After all unions, 6, 8, and 12 are all connected through their shared factor 2, forming one component of size 3.
The time complexity is O(n * √max(nums))
for factorization, where n
is the array length. The space complexity is O(max(nums))
for the Union-Find structure.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with nums = [20, 50, 9, 63]
.
Step 1: Initialize Union-Find Create a Union-Find structure with size 64 (max(nums) + 1). Each element starts as its own parent.
Step 2: Process each number and find factors
Processing 20:
- Check i = 2: 20 % 2 = 0 ✓
- union(20, 2)
- union(20, 10)
- Check i = 3: 20 % 3 ≠ 0 ✗
- Check i = 4: 20 % 4 = 0 ✓
- union(20, 4)
- union(20, 5)
- Stop (i = 5 > √20)
Current components: {20, 2, 10, 4, 5}
Processing 50:
- Check i = 2: 50 % 2 = 0 ✓
- union(50, 2)
- union(50, 25)
- Check i = 3: 50 % 3 ≠ 0 ✗
- Check i = 4: 50 % 4 ≠ 0 ✗
- Check i = 5: 50 % 5 = 0 ✓
- union(50, 5)
- union(50, 10)
- Check i = 6: 50 % 6 ≠ 0 ✗
- Check i = 7: 50 % 7 ≠ 0 ✗
- Stop (i = 8 > √50)
Now 50 connects to the existing component through factors 2, 5, and 10. Current components: {20, 50, 2, 10, 4, 5, 25}
Processing 9:
- Check i = 2: 9 % 2 ≠ 0 ✗
- Check i = 3: 9 % 3 = 0 ✓
- union(9, 3)
- union(9, 3) (since 9/3 = 3)
- Stop (i = 4 > √9)
9 forms a new component: {9, 3}
Processing 63:
- Check i = 2: 63 % 2 ≠ 0 ✗
- Check i = 3: 63 % 3 = 0 ✓
- union(63, 3)
- union(63, 21)
- Check i = 4: 63 % 4 ≠ 0 ✗
- Check i = 5: 63 % 5 ≠ 0 ✗
- Check i = 6: 63 % 6 ≠ 0 ✗
- Check i = 7: 63 % 7 = 0 ✓
- union(63, 7)
- union(63, 9)
- Stop (i = 8 > √63)
63 connects to 9's component through factor 3 and also through 9 itself. Current components: {9, 63, 3, 21, 7}
Step 3: Count component sizes Find the root for each original number:
- find(20) → root1
- find(50) → root1 (same component as 20)
- find(9) → root2
- find(63) → root2 (same component as 9)
Component sizes:
- Component with root1: contains {20, 50} → size 2
- Component with root2: contains {9, 63} → size 2
Answer: 2 (both components have the same size)
The key insight demonstrated here is how shared factors act as bridges. Numbers 20 and 50 are connected through their common factors (2, 5, 10), while 9 and 63 are connected through their common factor 3. The Union-Find structure efficiently manages these connections without explicitly checking every pair of numbers.
Solution Implementation
1from typing import List
2from collections import Counter
3
4
5class UnionFind:
6 """Union-Find (Disjoint Set Union) data structure with path compression."""
7
8 def __init__(self, n: int) -> None:
9 """Initialize n disjoint sets, each element is its own parent initially."""
10 self.parent = list(range(n))
11
12 def union(self, a: int, b: int) -> None:
13 """Unite the sets containing elements a and b."""
14 root_a = self.find(a)
15 root_b = self.find(b)
16
17 # Only unite if they belong to different sets
18 if root_a != root_b:
19 self.parent[root_a] = root_b
20
21 def find(self, x: int) -> int:
22 """Find the root of the set containing x with path compression."""
23 if self.parent[x] != x:
24 # Path compression: make every node point directly to the root
25 self.parent[x] = self.find(self.parent[x])
26 return self.parent[x]
27
28
29class Solution:
30 def largestComponentSize(self, nums: List[int]) -> int:
31 """
32 Find the size of the largest connected component where two numbers
33 are connected if they share a common factor greater than 1.
34
35 Args:
36 nums: List of positive integers
37
38 Returns:
39 Size of the largest connected component
40 """
41 # Create UnionFind structure with size equal to max value + 1
42 max_value = max(nums)
43 union_find = UnionFind(max_value + 1)
44
45 # For each number, connect it with all its factors
46 for num in nums:
47 # Find all factors by iterating up to sqrt(num)
48 factor = 2
49 while factor * factor <= num:
50 if num % factor == 0:
51 # Connect num with its factor
52 union_find.union(num, factor)
53 # Connect num with its complementary factor (num // factor)
54 union_find.union(num, num // factor)
55 factor += 1
56
57 # Count the size of each connected component
58 # by finding the root of each number and counting occurrences
59 component_sizes = Counter(union_find.find(num) for num in nums)
60
61 # Return the size of the largest component
62 return max(component_sizes.values())
63
1/**
2 * Union-Find (Disjoint Set Union) data structure implementation
3 * Used to efficiently manage and query connected components
4 */
5class UnionFind {
6 // Parent array where parent[i] represents the parent of node i
7 int[] parent;
8
9 /**
10 * Initialize Union-Find structure with n elements
11 * Initially, each element is its own parent (separate component)
12 * @param n number of elements
13 */
14 UnionFind(int n) {
15 parent = new int[n];
16 for (int i = 0; i < n; ++i) {
17 parent[i] = i;
18 }
19 }
20
21 /**
22 * Unite two elements into the same component
23 * @param a first element
24 * @param b second element
25 */
26 void union(int a, int b) {
27 int rootA = find(a);
28 int rootB = find(b);
29 // Only unite if they belong to different components
30 if (rootA != rootB) {
31 parent[rootA] = rootB;
32 }
33 }
34
35 /**
36 * Find the root parent of element x with path compression
37 * Path compression optimizes future queries by directly connecting nodes to root
38 * @param x element to find root for
39 * @return root parent of element x
40 */
41 int find(int x) {
42 // Path compression: make every node point directly to root
43 if (parent[x] != x) {
44 parent[x] = find(parent[x]);
45 }
46 return parent[x];
47 }
48}
49
50/**
51 * Solution class for finding the largest component size by common factor
52 * Groups numbers that share common factors greater than 1
53 */
54class Solution {
55 /**
56 * Find the size of the largest connected component where numbers are connected
57 * if they share a common factor greater than 1
58 * @param nums array of positive integers
59 * @return size of the largest connected component
60 */
61 public int largestComponentSize(int[] nums) {
62 // Find the maximum value to determine Union-Find size
63 int maxValue = 0;
64 for (int num : nums) {
65 maxValue = Math.max(maxValue, num);
66 }
67
68 // Create Union-Find structure for all possible values up to maxValue
69 UnionFind unionFind = new UnionFind(maxValue + 1);
70
71 // For each number, connect it with all its factors
72 for (int num : nums) {
73 // Check all potential factors up to sqrt(num)
74 int factor = 2;
75 while (factor * factor <= num) {
76 if (num % factor == 0) {
77 // Connect number with its factor
78 unionFind.union(num, factor);
79 // Connect number with its complementary factor
80 unionFind.union(num, num / factor);
81 }
82 ++factor;
83 }
84 }
85
86 // Count the size of each component
87 int[] componentSize = new int[maxValue + 1];
88 int maxComponentSize = 0;
89
90 // For each number, increment the count of its root component
91 for (int num : nums) {
92 int root = unionFind.find(num);
93 ++componentSize[root];
94 maxComponentSize = Math.max(maxComponentSize, componentSize[root]);
95 }
96
97 return maxComponentSize;
98 }
99}
100
1class UnionFind {
2public:
3 vector<int> parent;
4 int size;
5
6 // Initialize UnionFind with n elements, each element is its own parent initially
7 UnionFind(int n) : size(n), parent(n) {
8 // Set each element to be its own parent (0 -> 0, 1 -> 1, ..., n-1 -> n-1)
9 iota(parent.begin(), parent.end(), 0);
10 }
11
12 // Unite two elements by connecting their root parents
13 void unite(int a, int b) {
14 int rootA = find(a);
15 int rootB = find(b);
16 if (rootA != rootB) {
17 parent[rootA] = rootB; // Make rootB the parent of rootA
18 }
19 }
20
21 // Find the root parent of element x with path compression
22 int find(int x) {
23 if (parent[x] != x) {
24 parent[x] = find(parent[x]); // Path compression: directly connect x to root
25 }
26 return parent[x];
27 }
28};
29
30class Solution {
31public:
32 int largestComponentSize(vector<int>& nums) {
33 // Find the maximum value in nums to determine the range
34 int maxValue = *max_element(nums.begin(), nums.end());
35
36 // Create UnionFind structure for all numbers from 0 to maxValue
37 UnionFind* unionFind = new UnionFind(maxValue + 1);
38
39 // For each number, unite it with all its factors
40 for (int num : nums) {
41 // Check all potential factors from 2 to sqrt(num)
42 for (int factor = 2; factor * factor <= num; ++factor) {
43 if (num % factor == 0) {
44 // If factor divides num, unite num with both factor and num/factor
45 unionFind->unite(num, factor);
46 unionFind->unite(num, num / factor);
47 }
48 }
49 }
50
51 // Count the size of each component
52 vector<int> componentSize(maxValue + 1, 0);
53 int maxComponentSize = 0;
54
55 // For each number in nums, find its root and count component sizes
56 for (int num : nums) {
57 int root = unionFind->find(num);
58 ++componentSize[root];
59 maxComponentSize = max(maxComponentSize, componentSize[root]);
60 }
61
62 delete unionFind;
63 return maxComponentSize;
64 }
65};
66
1// Parent array for Union-Find structure
2let parent: number[];
3
4/**
5 * Initialize Union-Find with n elements
6 * Each element is its own parent initially
7 */
8function initializeUnionFind(n: number): void {
9 parent = new Array(n);
10 // Set each element to be its own parent (0 -> 0, 1 -> 1, ..., n-1 -> n-1)
11 for (let i = 0; i < n; i++) {
12 parent[i] = i;
13 }
14}
15
16/**
17 * Unite two elements by connecting their root parents
18 */
19function unite(a: number, b: number): void {
20 const rootA = find(a);
21 const rootB = find(b);
22 if (rootA !== rootB) {
23 // Make rootB the parent of rootA
24 parent[rootA] = rootB;
25 }
26}
27
28/**
29 * Find the root parent of element x with path compression
30 */
31function find(x: number): number {
32 if (parent[x] !== x) {
33 // Path compression: directly connect x to root
34 parent[x] = find(parent[x]);
35 }
36 return parent[x];
37}
38
39/**
40 * Find the size of the largest component where elements share a common factor > 1
41 */
42function largestComponentSize(nums: number[]): number {
43 // Find the maximum value in nums to determine the range
44 const maxValue = Math.max(...nums);
45
46 // Create Union-Find structure for all numbers from 0 to maxValue
47 initializeUnionFind(maxValue + 1);
48
49 // For each number, unite it with all its factors
50 for (const num of nums) {
51 // Check all potential factors from 2 to sqrt(num)
52 for (let factor = 2; factor * factor <= num; factor++) {
53 if (num % factor === 0) {
54 // If factor divides num, unite num with both factor and num/factor
55 unite(num, factor);
56 unite(num, Math.floor(num / factor));
57 }
58 }
59 }
60
61 // Count the size of each component
62 const componentSize = new Array(maxValue + 1).fill(0);
63 let maxComponentSize = 0;
64
65 // For each number in nums, find its root and count component sizes
66 for (const num of nums) {
67 const root = find(num);
68 componentSize[root]++;
69 maxComponentSize = Math.max(maxComponentSize, componentSize[root]);
70 }
71
72 return maxComponentSize;
73}
74
Time and Space Complexity
Time Complexity: O(n * √m * α(m))
where n
is the length of the input array, m
is the maximum value in the array, and α
is the inverse Ackermann function.
- The UnionFind initialization takes
O(m)
time to create the parent array. - For each number
v
innums
(n iterations):- The factorization loop runs from
i = 2
to√v
, which isO(√v)
iterations in the worst case. - Inside the loop, we check if
i
is a factor and perform at most 2 union operations. - Each union operation involves two find operations, and each find operation with path compression has amortized time complexity of
O(α(m))
.
- The factorization loop runs from
- The final step uses Counter on n elements, where each element requires a find operation:
O(n * α(m))
. - Overall:
O(m) + O(n * √m * α(m)) + O(n * α(m))
=O(n * √m * α(m))
since√m
dominates.
Space Complexity: O(m)
- The UnionFind structure uses an array of size
m + 1
to store parent pointers:O(m)
. - The Counter in the final step stores at most
n
entries:O(n)
. - The recursion stack for find operations can go up to
O(log m)
depth before path compression flattens it. - Since typically
m > n
in this problem context, the overall space complexity isO(m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Inefficient Factorization for Prime Numbers
The Problem: The current implementation iterates through all potential factors from 2 to √n for every number. For large prime numbers (e.g., 99991), this means checking thousands of potential factors unnecessarily. Since prime numbers have no factors other than 1 and themselves, they form singleton components, making all this computation wasteful.
Example:
If nums = [99991, 99989, 100003]
(all prime), the algorithm will check approximately √100000 ≈ 316 potential factors for each number, performing nearly 1000 modulo operations total, when we could determine they're prime much faster.
Solution: Pre-compute prime factorization or use a sieve to identify primes beforehand:
def largestComponentSize(self, nums: List[int]) -> int:
max_value = max(nums)
union_find = UnionFind(max_value + 1)
# Pre-compute smallest prime factor for each number using sieve
spf = list(range(max_value + 1)) # smallest prime factor
for i in range(2, int(max_value**0.5) + 1):
if spf[i] == i: # i is prime
for j in range(i * i, max_value + 1, i):
if spf[j] == j:
spf[j] = i
# Use SPF for efficient factorization
for num in nums:
factors_seen = set()
temp = num
while temp > 1:
prime_factor = spf[temp]
if prime_factor not in factors_seen:
union_find.union(num, prime_factor)
factors_seen.add(prime_factor)
temp //= prime_factor
component_sizes = Counter(union_find.find(num) for num in nums)
return max(component_sizes.values())
Pitfall 2: Memory Inefficiency for Sparse Large Values
The Problem:
The Union-Find structure allocates space for max(nums) + 1
elements. If nums = [2, 1000000]
, we allocate an array of size 1,000,001 but only use a tiny fraction of it. This wastes memory and can cause issues with memory limits.
Example:
For nums = [2, 999983]
, we allocate ~1M integers (4MB+) but only need space for 2 numbers and their factors.
Solution: Use a dictionary-based Union-Find that only stores actually used values:
class UnionFindDict:
def __init__(self):
self.parent = {}
def find(self, x):
if x not in self.parent:
self.parent[x] = x
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, a, b):
root_a, root_b = self.find(a), self.find(b)
if root_a != root_b:
self.parent[root_a] = root_b
Pitfall 3: Not Handling Edge Cases
The Problem: The code doesn't explicitly handle edge cases like:
- Single element arrays (should return 1)
- Arrays where all numbers are coprime (each forms its own component)
- Numbers that equal 1 (though the problem states positive integers)
Solution: Add explicit edge case handling:
def largestComponentSize(self, nums: List[int]) -> int:
if len(nums) <= 1:
return len(nums)
# Filter out 1 if present (1 shares no common factors > 1)
nums = [n for n in nums if n > 1]
if not nums:
return 1 if len(nums) > 0 else 0
# ... rest of the implementation
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
Recommended Readings
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
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!