Facebook Pixel

3378. Count Connected Components in LCM Graph

HardUnion FindArrayHash TableMathNumber Theory
Leetcode Link

Problem Description

You have an array nums containing n integers and a positive integer threshold.

Create a graph with n nodes where:

  • Node i has value nums[i]
  • Two nodes i and j are connected by an undirected edge if lcm(nums[i], nums[j]) <= threshold

The least common multiple (LCM) of two numbers is the smallest positive integer that is divisible by both numbers.

Your task is to count the number of connected components in this graph.

A connected component is a group of nodes where:

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

Example breakdown:

  • If nums = [2, 3, 6] and threshold = 10:
    • lcm(2, 3) = 6 <= 10 → nodes with values 2 and 3 are connected
    • lcm(2, 6) = 6 <= 10 → nodes with values 2 and 6 are connected
    • lcm(3, 6) = 6 <= 10 → nodes with values 3 and 6 are connected
    • All three nodes form one connected component, so return 1

The solution uses Union-Find (Disjoint Set Union) to efficiently group nodes. For each number in nums, it unions that number with all its multiples up to the threshold. This works because if two numbers share a common multiple within the threshold, they can be connected through that multiple. Numbers greater than the threshold cannot connect to anything and form isolated components.

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

Intuition

The key insight is recognizing that we don't need to check every pair of numbers to determine if they're connected. Instead, we can leverage the relationship between numbers and their multiples.

Consider two numbers a and b. Their LCM can be calculated as lcm(a, b) = (a * b) / gcd(a, b). For the LCM to be within the threshold, these numbers must share some mathematical relationship.

Here's the crucial observation: if two numbers a and b have lcm(a, b) <= threshold, then their LCM is a common multiple of both numbers that's within the threshold. This means both a and b divide their LCM evenly.

Instead of checking all pairs directly, we can think differently:

  • For each number num in our array, all multiples of num up to the threshold (i.e., num, 2*num, 3*num, ...) are potential connection points
  • If two different numbers from our array share a common multiple within the threshold, they can be connected through that multiple

For example, if we have numbers 2 and 3 with threshold 10:

  • Multiples of 2 within threshold: 2, 4, 6, 8, 10
  • Multiples of 3 within threshold: 3, 6, 9
  • They share the multiple 6, which means lcm(2, 3) = 6 <= 10

This transforms our problem into a union-find problem where:

  1. We union each number with all its multiples up to the threshold
  2. Numbers that share common multiples will end up in the same connected component
  3. Numbers greater than the threshold can't have any valid LCM with other numbers (since their LCM would exceed the threshold), so they form isolated components

This approach is efficient because instead of checking O(n²) pairs, we only iterate through multiples of each number up to the threshold.

Learn more about Union Find and Math patterns.

Solution Approach

The solution uses Union-Find (Disjoint Set Union) data structure to efficiently group connected components.

Implementation Details:

  1. DSU (Disjoint Set Union) Class Setup:

    • Initialize parent and rank dictionaries for values from 0 to threshold
    • Each value initially points to itself as parent
    • Rank is used for union by rank optimization
  2. Path Compression in Find Operation:

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # Path compression
        return self.parent[x]
    • Recursively finds the root parent
    • Compresses the path by making each node point directly to the root
  3. Union by Rank:

    def union_set(self, u, v):
        u = self.find(u)
        v = self.find(v)
        if u != v:
            if self.rank[u] < self.rank[v]:
                u, v = v, u
            self.parent[v] = u
    • Always attach the smaller tree under the root of the larger tree
    • This keeps the tree balanced and operations efficient
  4. Main Algorithm:

    for num in nums:
        for j in range(num, threshold + 1, num):
            dsu.union_set(num, j)
    • For each number in nums, iterate through its multiples: num, 2*num, 3*num, ... up to threshold
    • Union the number with each of its multiples
    • This creates connections between numbers that share common multiples
  5. Counting Connected Components:

    unique_parents = set()
    for num in nums:
        if num > threshold:
            unique_parents.add(num)
        else:
            unique_parents.add(dsu.find(num))
    • For numbers greater than threshold: They form isolated components (can't connect to anything)
    • For numbers within threshold: Find their root parent using DSU
    • Use a set to count unique root parents, which gives us the number of connected components

Time Complexity: O(n × (threshold/min_num) × α(threshold)) where α is the inverse Ackermann function (practically constant)

Space Complexity: O(threshold) for the DSU structure

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 small example with nums = [6, 12, 10] and threshold = 20.

Step 1: Initialize DSU Create a DSU structure with parent pointers for values 0 to 20. Each value initially points to itself.

Step 2: Process each number and its multiples

For num = 6:

  • Multiples within threshold: 6, 12, 18
  • Union(6, 6) - no change
  • Union(6, 12) - connects 6 and 12
  • Union(6, 18) - connects 6 and 18
  • After this: 6, 12, and 18 are in the same group (let's say root is 6)

For num = 12:

  • Multiples within threshold: 12
  • Union(12, 12) - no change (12 is already connected to 6's group)
  • After this: 12 remains in the group with root 6

For num = 10:

  • Multiples within threshold: 10, 20
  • Union(10, 10) - no change
  • Union(10, 20) - connects 10 and 20
  • After this: 10 and 20 form their own group (let's say root is 10)

Step 3: Count connected components

  • Find parent of 6: returns 6 (root of first group)
  • Find parent of 12: returns 6 (same group as 6)
  • Find parent of 10: returns 10 (root of second group)
  • Unique parents: {6, 10}
  • Result: 2 connected components

Verification:

  • lcm(6, 12) = 12 ≤ 20 ✓ (connected through multiple 12)
  • lcm(6, 10) = 30 > 20 ✗ (not connected)
  • lcm(12, 10) = 60 > 20 ✗ (not connected)

This confirms we have 2 connected components: {6, 12} and {10}.

Solution Implementation

1class DSU:
2    """Disjoint Set Union (Union-Find) data structure for efficient set operations."""
3  
4    def __init__(self, n: int) -> None:
5        """
6        Initialize DSU with n elements (0 to n-1).
7      
8        Args:
9            n: Number of elements in the DSU
10        """
11        # Each element is initially its own parent (self-loop indicates root)
12        self.parent = {i: i for i in range(n)}
13        # Rank is used for union by rank optimization (tree height)
14        self.rank = {i: 0 for i in range(n)}
15
16    def make_set(self, v: int) -> None:
17        """
18        Create a new set containing only element v.
19      
20        Args:
21            v: Element to create a new set for
22        """
23        self.parent[v] = v
24        self.rank[v] = 1
25
26    def find(self, x: int) -> int:
27        """
28        Find the root/representative of the set containing element x.
29        Uses path compression optimization.
30      
31        Args:
32            x: Element to find the root for
33          
34        Returns:
35            Root element of the set containing x
36        """
37        # Path compression: make all nodes on path point directly to root
38        if self.parent[x] != x:
39            self.parent[x] = self.find(self.parent[x])
40        return self.parent[x]
41
42    def union_set(self, u: int, v: int) -> None:
43        """
44        Unite the sets containing elements u and v.
45        Uses union by rank optimization.
46      
47        Args:
48            u: First element
49            v: Second element
50        """
51        # Find roots of both elements
52        root_u = self.find(u)
53        root_v = self.find(v)
54      
55        # Only unite if they're in different sets
56        if root_u != root_v:
57            # Union by rank: attach smaller tree under larger tree
58            if self.rank[root_u] < self.rank[root_v]:
59                root_u, root_v = root_v, root_u
60          
61            # Make root_u the parent of root_v
62            self.parent[root_v] = root_u
63          
64            # Update rank if both trees had same height
65            if self.rank[root_u] == self.rank[root_v]:
66                self.rank[root_u] += 1
67
68
69class Solution:
70    """Solution class for counting connected components based on divisibility."""
71  
72    def countComponents(self, nums: list[int], threshold: int) -> int:
73        """
74        Count the number of connected components where numbers are connected
75        if they share a common divisor greater than 1 and less than or equal to threshold.
76      
77        Args:
78            nums: List of integers to group into components
79            threshold: Maximum value to consider for divisibility connections
80          
81        Returns:
82            Number of connected components
83        """
84        # Initialize DSU for values up to threshold
85        dsu = DSU(threshold + 1)
86      
87        # Connect numbers based on their multiples
88        for num in nums:
89            # Only process numbers within threshold
90            if num <= threshold:
91                # Connect num with all its multiples up to threshold
92                # Start from 2*num since num itself is already handled
93                for multiple in range(num * 2, threshold + 1, num):
94                    dsu.union_set(num, multiple)
95      
96        # Find unique component representatives
97        unique_components = set()
98      
99        for num in nums:
100            if num > threshold:
101                # Numbers greater than threshold form their own components
102                unique_components.add(num)
103            else:
104                # Find the root representative of the component
105                unique_components.add(dsu.find(num))
106      
107        # Return the total number of unique components
108        return len(unique_components)
109
1/**
2 * Disjoint Set Union (Union-Find) data structure implementation
3 * Used for efficiently managing disjoint sets and performing union operations
4 */
5class DSU {
6    // Maps each element to its parent in the disjoint set tree
7    private Map<Integer, Integer> parent;
8    // Maps each element to its rank (used for union by rank optimization)
9    private Map<Integer, Integer> rank;
10
11    /**
12     * Initialize DSU with elements from 0 to n
13     * @param n The maximum element value
14     */
15    public DSU(int n) {
16        parent = new HashMap<>();
17        rank = new HashMap<>();
18      
19        // Initially, each element is its own parent (self-loop)
20        // All elements start with rank 0
21        for (int i = 0; i <= n; i++) {
22            parent.put(i, i);
23            rank.put(i, 0);
24        }
25    }
26
27    /**
28     * Create a new set containing only element v
29     * @param v The element to create a set for
30     */
31    public void makeSet(int v) {
32        parent.put(v, v);
33        rank.put(v, 1);
34    }
35
36    /**
37     * Find the root/representative of the set containing element x
38     * Uses path compression optimization for better performance
39     * @param x The element to find the root for
40     * @return The root element of the set containing x
41     */
42    public int find(int x) {
43        // Path compression: Make all nodes on the path point directly to root
44        if (parent.get(x) != x) {
45            parent.put(x, find(parent.get(x)));
46        }
47        return parent.get(x);
48    }
49
50    /**
51     * Union two sets containing elements u and v
52     * Uses union by rank to maintain balanced trees
53     * @param u First element
54     * @param v Second element
55     */
56    public void unionSet(int u, int v) {
57        // Find roots of both elements
58        int rootU = find(u);
59        int rootV = find(v);
60      
61        // If already in same set, no union needed
62        if (rootU != rootV) {
63            // Union by rank: attach smaller rank tree under root of higher rank tree
64            if (rank.get(rootU) < rank.get(rootV)) {
65                // Swap to ensure rootU has higher or equal rank
66                int temp = rootU;
67                rootU = rootV;
68                rootV = temp;
69            }
70          
71            // Make rootU the parent of rootV
72            parent.put(rootV, rootU);
73          
74            // Update rank if both trees had same rank
75            if (rank.get(rootU).equals(rank.get(rootV))) {
76                rank.put(rootU, rank.get(rootU) + 1);
77            }
78        }
79    }
80}
81
82/**
83 * Solution class to count connected components based on common divisors
84 */
85class Solution {
86    /**
87     * Count the number of connected components formed by numbers sharing common divisors
88     * Numbers are connected if they share a common divisor <= threshold
89     * @param nums Array of numbers to process
90     * @param threshold Maximum value to consider for common divisors
91     * @return Number of connected components
92     */
93    public int countComponents(int[] nums, int threshold) {
94        // Initialize DSU with size up to threshold
95        DSU dsu = new DSU(threshold);
96
97        // Connect numbers through their multiples
98        for (int num : nums) {
99            // For each number, connect it with all its multiples up to threshold
100            // This effectively groups numbers that share common divisors
101            for (int multiple = num; multiple <= threshold; multiple += num) {
102                dsu.unionSet(num, multiple);
103            }
104        }
105
106        // Count unique components
107        Set<Integer> uniqueParents = new HashSet<>();
108      
109        for (int num : nums) {
110            if (num > threshold) {
111                // Numbers greater than threshold form their own component
112                uniqueParents.add(num);
113            } else {
114                // Find the root of the component containing this number
115                uniqueParents.add(dsu.find(num));
116            }
117        }
118
119        // Return the total number of unique components
120        return uniqueParents.size();
121    }
122}
123
1// Disjoint Set Union (Union-Find) data structure with path compression and union by rank
2struct DSU {
3    unordered_map<int, int> parent;  // Maps element to its parent
4    unordered_map<int, int> rank;    // Rank for union by rank optimization
5  
6    // Constructor: Initialize DSU with n elements (0 to n-1)
7    DSU(int n) {
8        for (int i = 0; i < n; ++i) {
9            parent[i] = i;    // Each element is initially its own parent
10            rank[i] = 0;      // Initial rank is 0
11        }
12    }
13  
14    // Create a new set with element v as its representative
15    void makeSet(int v) {
16        parent[v] = v;
17        rank[v] = 1;
18    }
19  
20    // Find the representative of the set containing x with path compression
21    int find(int x) {
22        if (parent[x] == x) {
23            return x;  // x is the representative
24        }
25        // Path compression: make all nodes point directly to the root
26        return parent[x] = find(parent[x]);
27    }
28  
29    // Unite the sets containing u and v using union by rank
30    void unionSet(int u, int v) {
31        u = find(u);  // Find representative of u's set
32        v = find(v);  // Find representative of v's set
33      
34        if (u != v) {  // Only unite if they're in different sets
35            // Union by rank: attach smaller tree under root of larger tree
36            if (rank[u] < rank[v]) {
37                swap(u, v);
38            }
39            parent[v] = u;  // Make u the parent of v
40          
41            // Update rank if both trees had same rank
42            if (rank[u] == rank[v]) {
43                rank[u]++;
44            }
45        }
46    }
47};
48
49class Solution {
50public:
51    // Count the number of connected components after grouping numbers by GCD > 1
52    int countComponents(vector<int>& nums, int threshold) {
53        // Initialize DSU with elements from 0 to threshold
54        DSU dsu(threshold + 1);
55      
56        // Group numbers that share common factors
57        // For each number, unite it with all its multiples up to threshold
58        for (auto& num : nums) {
59            // Connect num with all its multiples (num, 2*num, 3*num, ...)
60            for (int multiple = num; multiple <= threshold; multiple += num) {
61                dsu.unionSet(num, multiple);
62            }
63        }
64      
65        // Count unique components by finding representative of each number
66        unordered_set<int> uniqueComponents;
67        for (auto& num : nums) {
68            if (num > threshold) {
69                // Numbers greater than threshold form their own component
70                uniqueComponents.insert(num);
71            } else {
72                // Find the representative of the component containing num
73                uniqueComponents.insert(dsu.find(num));
74            }
75        }
76      
77        // Return the total number of unique components
78        return uniqueComponents.size();
79    }
80};
81
1// Disjoint Set Union (Union-Find) data structure with path compression and union by rank
2const parent: Map<number, number> = new Map();  // Maps element to its parent
3const rank: Map<number, number> = new Map();     // Rank for union by rank optimization
4
5// Initialize DSU with n elements (0 to n-1)
6function initializeDSU(n: number): void {
7    for (let i = 0; i < n; i++) {
8        parent.set(i, i);    // Each element is initially its own parent
9        rank.set(i, 0);      // Initial rank is 0
10    }
11}
12
13// Create a new set with element v as its representative
14function makeSet(v: number): void {
15    parent.set(v, v);
16    rank.set(v, 1);
17}
18
19// Find the representative of the set containing x with path compression
20function find(x: number): number {
21    if (parent.get(x) === x) {
22        return x;  // x is the representative
23    }
24    // Path compression: make all nodes point directly to the root
25    const root = find(parent.get(x)!);
26    parent.set(x, root);
27    return root;
28}
29
30// Unite the sets containing u and v using union by rank
31function unionSet(u: number, v: number): void {
32    u = find(u);  // Find representative of u's set
33    v = find(v);  // Find representative of v's set
34  
35    if (u !== v) {  // Only unite if they're in different sets
36        // Union by rank: attach smaller tree under root of larger tree
37        if (rank.get(u)! < rank.get(v)!) {
38            [u, v] = [v, u];  // Swap u and v
39        }
40        parent.set(v, u);  // Make u the parent of v
41      
42        // Update rank if both trees had same rank
43        if (rank.get(u) === rank.get(v)) {
44            rank.set(u, rank.get(u)! + 1);
45        }
46    }
47}
48
49// Count the number of connected components after grouping numbers by GCD > 1
50function countComponents(nums: number[], threshold: number): number {
51    // Clear and initialize DSU with elements from 0 to threshold
52    parent.clear();
53    rank.clear();
54    initializeDSU(threshold + 1);
55  
56    // Group numbers that share common factors
57    // For each number, unite it with all its multiples up to threshold
58    for (const num of nums) {
59        // Connect num with all its multiples (num, 2*num, 3*num, ...)
60        for (let multiple = num; multiple <= threshold; multiple += num) {
61            unionSet(num, multiple);
62        }
63    }
64  
65    // Count unique components by finding representative of each number
66    const uniqueComponents: Set<number> = new Set();
67    for (const num of nums) {
68        if (num > threshold) {
69            // Numbers greater than threshold form their own component
70            uniqueComponents.add(num);
71        } else {
72            // Find the representative of the component containing num
73            uniqueComponents.add(find(num));
74        }
75    }
76  
77    // Return the total number of unique components
78    return uniqueComponents.size;
79}
80

Time and Space Complexity

Time Complexity: O(n * threshold * log(threshold)) where n is the length of the nums array.

  • The outer loop iterates through each number in nums: O(n)
  • For each number num, the inner loop iterates from num to threshold with step size num, which gives approximately threshold/num iterations
  • The total number of union operations across all numbers is bounded by O(threshold * log(threshold)) because the sum of threshold/1 + threshold/2 + ... + threshold/threshold equals threshold * H(threshold) where H(threshold) is the harmonic series, which is approximately O(log(threshold))
  • Each union operation involves two find operations, and with path compression, the amortized time complexity of find is nearly O(1), but worst case is O(log(threshold))
  • Therefore, the overall time complexity is O(n * threshold * log(threshold)) in the worst case

Space Complexity: O(threshold)

  • The DSU data structure maintains two dictionaries (parent and rank), each storing threshold + 1 elements: O(threshold)
  • The unique_parents set can store at most n elements (one for each number in nums): O(n)
  • Since typically n ≤ threshold in practical scenarios, the dominant space complexity is O(threshold)
  • The recursion stack for the find operation with path compression has a maximum depth of O(log(threshold)) in the worst case
  • Overall space complexity: O(threshold)

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

Common Pitfalls

1. Incorrect Connection Logic - Connecting Numbers Directly Instead of Through Multiples

The Pitfall: A common misunderstanding is trying to connect numbers directly by checking if their LCM is within the threshold:

# INCORRECT approach - directly checking LCM between pairs
for i in range(len(nums)):
    for j in range(i + 1, len(nums)):
        if lcm(nums[i], nums[j]) <= threshold:
            dsu.union_set(nums[i], nums[j])

Why This Fails:

  • The DSU structure is initialized for values 0 to threshold, not for the indices of nums
  • This would cause KeyError when nums[i] or nums[j] exceeds the DSU's initialized range
  • Even if you initialize DSU with all values in nums, you miss transitive connections through common multiples

The Correct Solution: Connect numbers through their common multiples within the threshold range:

# CORRECT approach - connecting through multiples
for num in nums:
    if num <= threshold:
        for multiple in range(num * 2, threshold + 1, num):
            dsu.union_set(num, multiple)

2. Starting Multiples from the Wrong Value

The Pitfall: Starting the multiple iteration from num itself instead of 2*num:

# INCORRECT - creates unnecessary self-loops
for multiple in range(num, threshold + 1, num):  # Starts at num
    dsu.union_set(num, multiple)

Why This Matters:

  • union_set(num, num) is redundant since every element is already its own parent
  • While not causing incorrect results, it adds unnecessary operations

The Correct Solution: Start from 2*num since the first actual multiple is twice the number:

# CORRECT - starts from first actual multiple
for multiple in range(num * 2, threshold + 1, num):
    dsu.union_set(num, multiple)

3. Not Handling Numbers Greater Than Threshold

The Pitfall: Forgetting that numbers greater than threshold cannot have any valid LCM connections:

# INCORRECT - tries to find parent for numbers > threshold
unique_components = set()
for num in nums:
    unique_components.add(dsu.find(num))  # Fails if num > threshold

Why This Fails:

  • Numbers greater than threshold aren't in the DSU structure
  • They can't connect to any other number (their LCM with anything would exceed threshold)
  • Calling dsu.find(num) when num > threshold causes KeyError

The Correct Solution: Treat numbers greater than threshold as isolated components:

# CORRECT - handles large numbers separately
unique_components = set()
for num in nums:
    if num > threshold:
        unique_components.add(num)  # Each forms its own component
    else:
        unique_components.add(dsu.find(num))

4. Misunderstanding the Connection Criteria

The Pitfall: Thinking that numbers connect only if they directly divide each other or share a GCD:

# INCORRECT understanding - checking GCD instead of LCM
for i in range(len(nums)):
    for j in range(i + 1, len(nums)):
        if gcd(nums[i], nums[j]) > 1:  # Wrong criteria
            # connect them

The Key Insight: Two numbers are connected if lcm(a, b) <= threshold. The solution cleverly uses the fact that if two numbers share a common multiple within the threshold, they can be connected through that multiple. For example:

  • Numbers 2 and 3 both divide 6
  • If 6 ≤ threshold, then 2 and 3 are in the same component
  • This is why we connect each number to all its multiples up to the threshold
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?


Recommended Readings

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

Load More