Facebook Pixel

1627. Graph Connectivity With Threshold

Problem Description

You are given n cities labeled from 1 to n. Two cities are connected by a road if they share a common divisor greater than a given threshold. Specifically, cities x and y are directly connected if there exists an integer z such that:

  • x % z == 0 (z divides x)
  • y % z == 0 (z divides y)
  • z > threshold

Given n, threshold, and an array of queries where each query is [ai, bi], you need to determine if cities ai and bi are connected either directly or indirectly (through a path of roads).

The solution uses Union-Find to efficiently group connected cities. The key insight is to iterate through all possible common divisors a starting from threshold + 1 up to n. For each divisor a, we connect all its multiples within the range [1, n] by unioning a with 2a, 3a, 4a, and so on. This is done in the nested loop where b iterates through multiples of a (starting from a + a and incrementing by a).

After building the connected components, each query [a, b] can be answered by checking if cities a and b belong to the same component using the find operation. If find(a) == find(b), the cities are connected and we return true; otherwise, we return false.

The Union-Find data structure with path compression and union by size ensures efficient operations, making the overall solution time-efficient for handling multiple queries.

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

Intuition

The key observation is that if two cities share a common divisor greater than the threshold, they are connected. Moreover, if city a is connected to city b, and city b is connected to city c, then a and c are also connected through b. This transitive property suggests we're dealing with connected components in a graph.

Instead of checking every pair of cities to see if they share a common divisor (which would be inefficient), we can think about the problem from the perspective of the divisors themselves. For any divisor d > threshold, all multiples of d that are ≤ n will be connected to each other because they all share d as a common divisor.

For example, if threshold = 2 and n = 12, then for divisor 3, cities 3, 6, 9, 12 all share divisor 3 and should be in the same connected component. Similarly, for divisor 4, cities 4, 8, 12 should be connected.

This leads us to the approach: for each potential divisor d from threshold + 1 to n, we can connect all its multiples. The clever part is that we don't need to connect every pair of multiples directly - we can just connect each multiple to the divisor itself (or connect consecutive multiples). This is because Union-Find will automatically handle the transitive connections.

The Union-Find data structure is perfect for this problem because:

  1. We need to build groups of connected cities efficiently
  2. We need to quickly query whether two cities are in the same group
  3. Cities might be connected through multiple different divisors (e.g., 6 and 12 share both 3 and 6 as common divisors), and Union-Find handles these overlapping groups naturally by merging them

Learn more about Union Find and Math patterns.

Solution Approach

The solution uses the Union-Find (Disjoint Set Union) data structure to efficiently group connected cities and answer connectivity queries.

Union-Find Implementation:

The UnionFind class maintains two arrays:

  • p: Parent array where p[x] stores the parent of node x
  • size: Size array to track the size of each component for union by size optimization

The find(x) method finds the root of the component containing x and applies path compression by directly connecting x to its root during the traversal. This optimization ensures nearly constant time operations.

The union(a, b) method merges two components by connecting the root of the smaller component to the root of the larger one (union by size). This keeps the tree balanced and prevents degradation to a linked list.

Main Algorithm:

  1. Initialize Union-Find: Create a Union-Find structure with n + 1 elements (to handle 1-indexed cities from 1 to n).

  2. Build Connected Components:

    • Iterate through all potential common divisors a from threshold + 1 to n
    • For each divisor a, iterate through its multiples: b = 2a, 3a, 4a, ... up to n
    • Union a with each of its multiples b using uf.union(a, b)
    • This effectively groups all numbers that share a as a common divisor

    The nested loop structure:

    for a in range(threshold + 1, n + 1):
        for b in range(a + a, n + 1, a):
            uf.union(a, b)

    For example, when a = 3, we union 3 with 6, 9, 12, etc. This connects all multiples of 3.

  3. Answer Queries: For each query [a, b], check if cities a and b are in the same connected component by comparing their roots: uf.find(a) == uf.find(b). Return True if they share the same root (connected), False otherwise.

Time Complexity:

  • Building components: O(n * log(n)) for iterating through divisors and their multiples
  • Answering queries: O(q * α(n)) where q is the number of queries and α is the inverse Ackermann function (nearly constant)

Space Complexity: O(n) for the Union-Find data 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 n = 6, threshold = 1, and queries [[4, 6], [3, 5], [3, 4]].

Step 1: Initialize Union-Find We create a Union-Find structure with parent array p = [0, 1, 2, 3, 4, 5, 6] where each city initially points to itself.

Step 2: Build Connected Components We iterate through divisors from threshold + 1 = 2 to n = 6:

  • Divisor 2: Connect all multiples of 2

    • Union(2, 4): Cities 2 and 4 now share component
    • Union(2, 6): Cities 2, 4, and 6 now share component
    • Component: {2, 4, 6}
  • Divisor 3: Connect all multiples of 3

    • Union(3, 6): City 3 joins the component containing 6
    • Since 6 was already connected to {2, 4}, we now have {2, 3, 4, 6}
  • Divisor 4: Connect all multiples of 4

    • No multiples of 4 within range (next would be 8 > 6)
  • Divisor 5: Connect all multiples of 5

    • No multiples of 5 within range (next would be 10 > 6)
  • Divisor 6: Connect all multiples of 6

    • No multiples of 6 within range (next would be 12 > 6)

After building, we have two components:

  • Component 1: {2, 3, 4, 6} (connected through shared divisors)
  • Component 2: {1} (no divisor > 1 divides 1)
  • Component 3: {5} (only divisible by 1 and 5, no connections)

Step 3: Answer Queries

  • Query [4, 6]: Find(4) = 2, Find(6) = 2 → Same root → True

    • Cities 4 and 6 share divisor 2 (both are even numbers)
  • Query [3, 5]: Find(3) = 2, Find(5) = 5 → Different roots → False

    • Cities 3 and 5 share no divisor greater than 1
  • Query [3, 4]: Find(3) = 2, Find(4) = 2 → Same root → True

    • Cities 3 and 4 are connected through 6 (3 and 6 share divisor 3, while 4 and 6 share divisor 2)

The key insight is that we don't need to find all common divisors between every pair of cities. Instead, by connecting each divisor to all its multiples, the Union-Find structure automatically creates the transitive connections. For instance, even though 3 and 4 don't share a common divisor > 1 directly, they're connected through their mutual connection to 6.

Solution Implementation

1from typing import List
2
3
4class UnionFind:
5    """
6    Union-Find (Disjoint Set Union) data structure with path compression 
7    and union by size optimization.
8    """
9  
10    def __init__(self, n: int) -> None:
11        """
12        Initialize the Union-Find structure with n elements (0 to n-1).
13      
14        Args:
15            n: Number of elements in the structure
16        """
17        # Each element is initially its own parent (represents n disjoint sets)
18        self.parent = list(range(n))
19        # Track the size of each component for union by size optimization
20        self.component_size = [1] * n
21  
22    def find(self, x: int) -> int:
23        """
24        Find the root/representative of the component containing element x.
25        Uses path compression to optimize future queries.
26      
27        Args:
28            x: The element whose root we want to find
29          
30        Returns:
31            The root element of the component containing x
32        """
33        # Path compression: make all nodes on the path point directly to the root
34        if self.parent[x] != x:
35            self.parent[x] = self.find(self.parent[x])
36        return self.parent[x]
37  
38    def union(self, a: int, b: int) -> bool:
39        """
40        Unite the components containing elements a and b.
41        Uses union by size to keep the tree balanced.
42      
43        Args:
44            a: First element
45            b: Second element
46          
47        Returns:
48            True if the elements were in different components (union performed),
49            False if they were already in the same component
50        """
51        # Find the roots of both elements
52        root_a, root_b = self.find(a), self.find(b)
53      
54        # Already in the same component
55        if root_a == root_b:
56            return False
57      
58        # Union by size: attach smaller tree under the root of larger tree
59        if self.component_size[root_a] > self.component_size[root_b]:
60            self.parent[root_b] = root_a
61            self.component_size[root_a] += self.component_size[root_b]
62        else:
63            self.parent[root_a] = root_b
64            self.component_size[root_b] += self.component_size[root_a]
65      
66        return True
67
68
69class Solution:
70    def areConnected(
71        self, n: int, threshold: int, queries: List[List[int]]
72    ) -> List[bool]:
73        """
74        Determine if pairs of numbers share a common divisor greater than threshold.
75        Two numbers are connected if they share a common divisor > threshold.
76      
77        Args:
78            n: Range of numbers from 1 to n
79            threshold: Minimum value for a valid common divisor
80            queries: List of [a, b] pairs to check for connectivity
81          
82        Returns:
83            List of boolean values indicating if each query pair is connected
84        """
85        # Initialize Union-Find with n+1 elements (to handle 1-indexed numbers)
86        union_find = UnionFind(n + 1)
87      
88        # Connect all numbers that share a common divisor > threshold
89        # For each potential divisor greater than threshold
90        for divisor in range(threshold + 1, n + 1):
91            # Connect all multiples of this divisor
92            # Start from 2*divisor and increment by divisor to get all multiples
93            for multiple in range(divisor + divisor, n + 1, divisor):
94                # Union the divisor with its multiple
95                # This creates connected components of numbers sharing common divisors
96                union_find.union(divisor, multiple)
97      
98        # Check each query pair to see if they're in the same component
99        # Two numbers are connected if they have the same root in the Union-Find
100        return [union_find.find(a) == union_find.find(b) for a, b in queries]
101
1/**
2 * Union-Find (Disjoint Set Union) data structure with path compression and union by size
3 */
4class UnionFind {
5    private int[] parent;  // parent[i] stores the parent of node i
6    private int[] size;     // size[i] stores the size of the tree rooted at i
7  
8    /**
9     * Initialize Union-Find structure with n elements (0 to n-1)
10     * @param n number of elements
11     */
12    public UnionFind(int n) {
13        parent = new int[n];
14        size = new int[n];
15      
16        // Initially, each element is its own parent (separate sets)
17        for (int i = 0; i < n; i++) {
18            parent[i] = i;
19            size[i] = 1;
20        }
21    }
22  
23    /**
24     * Find the root/representative of the set containing element x
25     * Uses path compression for optimization
26     * @param x the element to find
27     * @return the root of the set containing x
28     */
29    public int find(int x) {
30        // Path compression: make all nodes on path point directly to root
31        if (parent[x] != x) {
32            parent[x] = find(parent[x]);
33        }
34        return parent[x];
35    }
36  
37    /**
38     * Union two sets containing elements a and b
39     * Uses union by size for optimization
40     * @param a first element
41     * @param b second element
42     * @return true if union was performed, false if already in same set
43     */
44    public boolean union(int a, int b) {
45        int rootA = find(a);
46        int rootB = find(b);
47      
48        // Already in the same set
49        if (rootA == rootB) {
50            return false;
51        }
52      
53        // Union by size: attach smaller tree under larger tree
54        if (size[rootA] > size[rootB]) {
55            parent[rootB] = rootA;
56            size[rootA] += size[rootB];
57        } else {
58            parent[rootA] = rootB;
59            size[rootB] += size[rootA];
60        }
61      
62        return true;
63    }
64}
65
66/**
67 * Solution class for checking connectivity based on common divisors
68 */
69class Solution {
70    /**
71     * Determines if pairs of cities are connected through common divisors greater than threshold
72     * Two cities are connected if they share a common divisor > threshold
73     * @param n number of cities (1 to n)
74     * @param threshold minimum divisor value
75     * @param queries array of city pairs to check connectivity
76     * @return list of boolean values indicating connectivity for each query
77     */
78    public List<Boolean> areConnected(int n, int threshold, int[][] queries) {
79        // Create Union-Find for n+1 elements (cities numbered 1 to n)
80        UnionFind unionFind = new UnionFind(n + 1);
81      
82        // Connect all cities that share common divisors greater than threshold
83        // For each potential divisor starting from threshold+1
84        for (int divisor = threshold + 1; divisor <= n; divisor++) {
85            // Connect all multiples of this divisor
86            // Start from 2*divisor and increment by divisor to get all multiples
87            for (int multiple = divisor + divisor; multiple <= n; multiple += divisor) {
88                unionFind.union(divisor, multiple);
89            }
90        }
91      
92        // Process queries and check connectivity
93        List<Boolean> result = new ArrayList<>();
94        for (int[] query : queries) {
95            // Two cities are connected if they belong to the same set
96            boolean isConnected = unionFind.find(query[0]) == unionFind.find(query[1]);
97            result.add(isConnected);
98        }
99      
100        return result;
101    }
102}
103
1class UnionFind {
2public:
3    // Constructor: Initialize Union-Find data structure with n elements
4    UnionFind(int n) {
5        parent = vector<int>(n);
6        rank = vector<int>(n, 1);
7        // Initialize each element as its own parent (self-loop)
8        iota(parent.begin(), parent.end(), 0);
9    }
10
11    // Unite two sets containing elements a and b
12    // Returns true if they were in different sets, false if already in same set
13    bool unite(int a, int b) {
14        int rootA = find(a);
15        int rootB = find(b);
16      
17        // Already in the same set
18        if (rootA == rootB) {
19            return false;
20        }
21      
22        // Union by rank: attach smaller tree under root of larger tree
23        if (rank[rootA] > rank[rootB]) {
24            parent[rootB] = rootA;
25            rank[rootA] += rank[rootB];
26        } else {
27            parent[rootA] = rootB;
28            rank[rootB] += rank[rootA];
29        }
30      
31        return true;
32    }
33
34    // Find the root of the set containing element x with path compression
35    int find(int x) {
36        if (parent[x] != x) {
37            // Path compression: make every node point directly to root
38            parent[x] = find(parent[x]);
39        }
40        return parent[x];
41    }
42
43private:
44    vector<int> parent;  // Parent array for disjoint set
45    vector<int> rank;    // Rank/size array for union by rank optimization
46};
47
48class Solution {
49public:
50    // Check connectivity between pairs of nodes based on common divisors greater than threshold
51    vector<bool> areConnected(int n, int threshold, vector<vector<int>>& queries) {
52        // Initialize Union-Find with n+1 elements (1-indexed)
53        UnionFind unionFind(n + 1);
54      
55        // Connect all numbers that share a common divisor greater than threshold
56        // For each potential divisor starting from threshold+1
57        for (int divisor = threshold + 1; divisor <= n; ++divisor) {
58            // Connect all multiples of this divisor
59            for (int multiple = divisor + divisor; multiple <= n; multiple += divisor) {
60                unionFind.unite(divisor, multiple);
61            }
62        }
63      
64        // Process queries and check if pairs are connected
65        vector<bool> result;
66        for (auto& query : queries) {
67            // Two nodes are connected if they have the same root in Union-Find
68            result.push_back(unionFind.find(query[0]) == unionFind.find(query[1]));
69        }
70      
71        return result;
72    }
73};
74
1// Parent array for Union-Find structure
2let parent: number[];
3// Size array to track component sizes for union by rank
4let size: number[];
5
6/**
7 * Initialize Union-Find data structure
8 * @param n - Number of elements
9 */
10function initializeUnionFind(n: number): void {
11    // Initialize each element as its own parent
12    parent = Array(n)
13        .fill(0)
14        .map((_, index) => index);
15    // Initialize all component sizes to 1
16    size = Array(n).fill(1);
17}
18
19/**
20 * Find the root parent of element x with path compression
21 * @param x - Element to find root for
22 * @returns Root parent of element x
23 */
24function find(x: number): number {
25    // Path compression: make all nodes point directly to root
26    if (parent[x] !== x) {
27        parent[x] = find(parent[x]);
28    }
29    return parent[x];
30}
31
32/**
33 * Union two elements by connecting their root parents
34 * @param a - First element
35 * @param b - Second element
36 * @returns true if union was performed, false if already connected
37 */
38function union(a: number, b: number): boolean {
39    // Find root parents of both elements
40    const rootA = find(a);
41    const rootB = find(b);
42  
43    // Already in same component
44    if (rootA === rootB) {
45        return false;
46    }
47  
48    // Union by rank: attach smaller tree under larger tree
49    if (size[rootA] > size[rootB]) {
50        parent[rootB] = rootA;
51        size[rootA] += size[rootB];
52    } else {
53        parent[rootA] = rootB;
54        size[rootB] += size[rootA];
55    }
56  
57    return true;
58}
59
60/**
61 * Check if pairs of nodes are connected based on common divisors greater than threshold
62 * @param n - Number of nodes (1 to n)
63 * @param threshold - Minimum divisor value to consider
64 * @param queries - Array of node pairs to check connectivity
65 * @returns Array of boolean values indicating connectivity for each query
66 */
67function areConnected(n: number, threshold: number, queries: number[][]): boolean[] {
68    // Initialize Union-Find with n+1 elements (0 to n, where 0 is unused)
69    initializeUnionFind(n + 1);
70  
71    // Connect all numbers that share a common divisor greater than threshold
72    // Start from threshold + 1 as the divisor
73    for (let divisor = threshold + 1; divisor <= n; divisor++) {
74        // Connect all multiples of the current divisor
75        for (let multiple = divisor * 2; multiple <= n; multiple += divisor) {
76            union(divisor, multiple);
77        }
78    }
79  
80    // Check connectivity for each query pair
81    return queries.map(([nodeA, nodeB]) => find(nodeA) === find(nodeB));
82}
83

Time and Space Complexity

The time complexity is O(n × log n × α(n) + q × α(n)), which can be simplified to O(n × log n × α(n) + q) when considering that α(n) is nearly constant for practical values of n.

Breaking down the time complexity:

  • The nested loops in areConnected iterate through divisors: for each number a from threshold + 1 to n, we iterate through its multiples b. This creates approximately n/1 + n/2 + n/3 + ... + n/n iterations, which equals n × (1 + 1/2 + 1/3 + ... + 1/n) = O(n × log n) operations.
  • Each union operation takes O(α(n)) time due to path compression in the find operation, where α(n) is the inverse Ackermann function.
  • Processing q queries, each requiring two find operations, takes O(q × α(n)) time.

The space complexity is O(n):

  • The UnionFind data structure uses two arrays (p and size), each of size n + 1, requiring O(n) space.
  • No additional significant space is used beyond the input and output.

Here, n is the number of nodes, q is the number of queries, and α is the inverse Ackermann function, which grows extremely slowly and is effectively constant (≤ 4) for all practical values of n.

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

Common Pitfalls

1. Inefficient Connection Strategy

A common mistake is trying to connect cities by checking every pair of cities to see if they share a common divisor greater than the threshold. This would result in O(n²) comparisons and checking their GCD, leading to time complexity issues.

Incorrect Approach:

# Don't do this - inefficient O(n²) approach
for i in range(1, n + 1):
    for j in range(i + 1, n + 1):
        if gcd(i, j) > threshold:
            union_find.union(i, j)

Solution: Instead of checking all pairs, iterate through potential divisors and connect their multiples. This is more efficient because each number is visited only by its divisors.

2. Off-by-One Errors with Indexing

Since cities are labeled from 1 to n (1-indexed), but Python arrays are 0-indexed, it's easy to make indexing errors.

Common Mistake:

# Wrong: Using n elements for 1-indexed cities
union_find = UnionFind(n)  # This creates indices 0 to n-1
# Later accessing city n would cause IndexError

Solution: Initialize Union-Find with n + 1 elements to accommodate 1-indexed cities, effectively ignoring index 0.

3. Incorrect Loop Boundaries for Multiples

When connecting multiples of a divisor, starting from the wrong value or using wrong increment can miss connections.

Common Mistake:

# Wrong: Starting from divisor instead of 2*divisor
for divisor in range(threshold + 1, n + 1):
    for multiple in range(divisor, n + 1, divisor):  # Includes divisor itself
        union_find.union(divisor, multiple)

This unnecessarily tries to union a number with itself and can cause confusion.

Solution: Start from divisor + divisor (which is 2*divisor) to avoid self-unions and ensure we're only connecting distinct multiples.

4. Missing Path Compression in Find Operation

Implementing find without path compression leads to degraded performance over multiple queries.

Inefficient Implementation:

def find(self, x):
    # No path compression - can degrade to O(n) per operation
    while self.parent[x] != x:
        x = self.parent[x]
    return x

Solution: Use recursive path compression to flatten the tree structure:

def find(self, x):
    if self.parent[x] != x:
        self.parent[x] = self.find(self.parent[x])  # Path compression
    return self.parent[x]

5. Forgetting Union by Size/Rank

Without union by size or rank, the Union-Find tree can become unbalanced, leading to poor performance.

Poor Implementation:

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  # Always attach a to b

Solution: Maintain component sizes and always attach the smaller tree to the larger one to keep the tree balanced.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement recursion?


Recommended Readings

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

Load More