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.
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:
- We need to build groups of connected cities efficiently
- We need to quickly query whether two cities are in the same group
- Cities might be connected through multiple different divisors (e.g.,
6
and12
share both3
and6
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 wherep[x]
stores the parent of nodex
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:
-
Initialize Union-Find: Create a Union-Find structure with
n + 1
elements (to handle 1-indexed cities from1
ton
). -
Build Connected Components:
- Iterate through all potential common divisors
a
fromthreshold + 1
ton
- For each divisor
a
, iterate through its multiples:b = 2a, 3a, 4a, ...
up ton
- Union
a
with each of its multiplesb
usinguf.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 union3
with6
,9
,12
, etc. This connects all multiples of3
. - Iterate through all potential common divisors
-
Answer Queries: For each query
[a, b]
, check if citiesa
andb
are in the same connected component by comparing their roots:uf.find(a) == uf.find(b)
. ReturnTrue
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))
whereq
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 EvaluatorExample 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 numbera
fromthreshold + 1
ton
, we iterate through its multiplesb
. This creates approximatelyn/1 + n/2 + n/3 + ... + n/n
iterations, which equalsn × (1 + 1/2 + 1/3 + ... + 1/n)
=O(n × log n)
operations. - Each
union
operation takesO(α(n))
time due to path compression in thefind
operation, whereα(n)
is the inverse Ackermann function. - Processing
q
queries, each requiring twofind
operations, takesO(q × α(n))
time.
The space complexity is O(n)
:
- The UnionFind data structure uses two arrays (
p
andsize
), each of sizen + 1
, requiringO(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.
Which data structure is used to implement recursion?
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!