Problem Description
Given an array of unique integers and a positive integer threshold, we imagine a graph where each number is a node. An undirected edge exists between two nodes i and j if the least common multiple lcm(nums[i], nums[j]) is ≤ threshold. The task is to determine the number of connected components (i.e. groups of mutually “reachable” nodes) in the graph.
Key Insights
- If a number is larger than the threshold, then for any other number the lcm is at least that number – so nodes with nums[i] > threshold are isolated.
- For two numbers a and b that are both ≤ threshold, note that lcm(a, b) = (a * b) / gcd(a, b). In many cases (for example when one number divides the other) the condition can be checked more easily.
- When a divides b then lcm(a, b)=b so an edge exists provided b ≤ threshold.
- If neither number divides the other then typically (a * b) is “reduced” by the gcd; in fact, if both numbers are “sufficiently large” then in order for a·b/gcd(a,b) to be ≤ threshold the numbers must be very close (or share a very large common divisor). This suggests that aside from unioning numbers by “divisibility–style” connections (by iterating over multiples) it is only necessary to “double‐loop” over the very small numbers (say at most √(threshold)) and also check candidates that are nearly equal.
- The union–find (disjoint set union) data structure is ideal for “merging” nodes that are connected by a valid edge and later counting connected components.
Space and Time Complexity
Time Complexity: O(M log M + M · (threshold/divisor)) on the “small” part plus an extra double loop on O(√(threshold)) numbers, where M is the number of numbers ≤ threshold. (In worst‐case M is O(n) but most unions are done by iterating over multiples; the extra loops over very small numbers do not blow up.)
Space Complexity: O(n) for storing the union–find structure and hash maps.
Solution
We proceed in two steps:
- Pre–processing and “divisibility unions”: Notice that if a divides b and b ≤ threshold, then automatically lcm(a, b)=b ≤ threshold. Thus, we first “union” nodes by iterating over the input numbers that are ≤ threshold. Using a hash map from value to index, for each number v we iterate over multiples m (i.e. 2v, 3v, …, up to threshold) and if m appears in the input we union v and m.
- Catching additional “non‐divisible” unions: It is possible that two numbers a and b (both ≤ threshold) do not have a divisor/dividend relation yet still have lcm(a, b) ≤ threshold (for example, a and b may share a large common factor). One key observation is that if both a and b are “large” (greater than √(threshold)) then a*b is “normally” too big unless they are very close or share a huge gcd. Thus, we separately handle the “small” numbers – those ≤ √(threshold) (and possibly check a few adjacent numbers in sorted order for the remaining ones) – by a double–loop. For each pair from the very small group, we compute lcm and if it is ≤ threshold we union them.
- Finally, note that any number > threshold is isolated, so each one forms its own connected component. For the rest, after performing the unions the answer is the number of disjoint sets. The “gotchas” include carefully splitting the input based on value (relative to threshold) and recognizing that while in theory every pair among small nodes might be checked, in practice the cost is controlled by the constraint on threshold (and by only “double–looping” on numbers below √(threshold)).
Data structures used include: • A dictionary (or hash map) that maps a number (which is in the input and ≤ threshold) to its index. • The union–find data structure (with path compression and union–by–rank) to “merge” nodes that are connected. • (Optionally) A sorted array for the small numbers to “sweep through” adjacent nearly–equal values. The overall algorithm is a union–find based solution that “builds” the graph implicitly by enumerating candidate connecting edges based on number theory properties of divisibility and lcm.