Facebook Pixel

952. Largest Component Size by Common Factor

HardUnion FindArrayHash TableMathNumber Theory
Leetcode Link

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 have n nodes)
  • The nodes are labeled with the actual values from the array (not indices)
  • Two nodes nums[i] and nums[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
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 containing x. 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 containing a and b by making one root point to the other.

Main Algorithm:

  1. Initialize Union-Find: Create a Union-Find structure with size max(nums) + 1 to accommodate all possible values and their factors.

  2. Factor Finding and Union Operations: For each number v in the array:

    • Start checking potential factors from i = 2
    • Continue while i <= √v (written as i <= v // i to avoid floating-point operations)
    • If i divides v evenly (v % i == 0):
      • Union v with factor i
      • Union v with its complementary factor v // i
    • Increment i and continue
  3. 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

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 Evaluator

Example 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 in nums (n iterations):
    • The factorization loop runs from i = 2 to √v, which is O(√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 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 is O(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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

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

Load More