Facebook Pixel

1998. GCD Sort of an Array

Problem Description

You have an integer array nums that you want to sort in non-decreasing order (ascending order). However, you can only swap elements under a specific condition.

The swapping rule is: You can swap two elements nums[i] and nums[j] only if their greatest common divisor (GCD) is greater than 1. This means the two numbers must share at least one common factor other than 1.

For example:

  • You can swap 6 and 9 because gcd(6, 9) = 3 > 1
  • You can swap 4 and 10 because gcd(4, 10) = 2 > 1
  • You cannot swap 3 and 7 because gcd(3, 7) = 1

You can perform any number of these valid swaps. The question asks whether it's possible to sort the entire array into non-decreasing order using only these constrained swaps.

The key insight is that if two numbers share a common factor greater than 1, they can be swapped. Additionally, if number A can swap with B, and B can swap with C, then A and C are in the same "connected component" - they can eventually be rearranged among themselves through a series of swaps.

The solution uses Union-Find (Disjoint Set Union) to group numbers that can be swapped directly or indirectly. First, it finds all prime factors for each number in the array. Then it unions all numbers that share common prime factors. Finally, it checks if each number in the original array can reach its target position in the sorted array through the swap connections.

The function returns true if the array can be sorted using the allowed swaps, and false otherwise.

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

Intuition

The first observation is that if two numbers share a common factor greater than 1, they can be swapped. But this creates a transitive relationship - if number A can swap with B (they share factor 2), and B can swap with C (they share factor 3), then A and C can eventually swap positions through B as an intermediary.

This leads to thinking about connected components. Numbers that can swap directly or indirectly form groups. Within each group, we can rearrange the numbers in any order we want through a series of swaps.

For example, consider [6, 10, 15]:

  • 6 and 10 share factor 2, so they can swap
  • 6 and 15 share factor 3, so they can swap
  • 10 and 15 share factor 5, so they can swap
  • All three numbers are in the same group and can be arranged in any order

The key insight is: for the array to be sortable, each number must be able to reach its target position in the sorted array. This means that for each position, the number currently there and the number that should be there (in the sorted array) must be in the same connected component.

To efficiently track these connections, we can use Union-Find. But how do we connect numbers? We could check every pair of numbers for gcd > 1, but that's expensive.

A better approach: two numbers share a factor greater than 1 if and only if they share at least one prime factor. So we can:

  1. Find all prime factors for each number
  2. Group numbers by their prime factors
  3. Numbers sharing any prime factor belong to the same component

Finally, we check if the sorting is possible by verifying that each number can reach its sorted position through the connection network we've built.

Learn more about Union Find, Math and Sorting patterns.

Solution Approach

The solution uses Union-Find (Disjoint Set Union) data structure combined with prime factorization to solve this problem efficiently.

Step 1: Initialize Union-Find Structure

n = 10**5 + 10
p = list(range(n))

We create a parent array p where initially each element is its own parent. The size 10^5 + 10 covers the maximum possible value in the constraints.

Step 2: Build Prime Factor Mapping Using Sieve

f = defaultdict(list)
mx = max(nums)
for i in range(2, mx + 1):
    if f[i]:
        continue
    for j in range(i, mx + 1, i):
        f[j].append(i)

This implements a modified Sieve of Eratosthenes. For each number from 2 to the maximum value in nums:

  • If f[i] is empty, then i is prime
  • We mark all multiples of i (including i itself) as having i as a prime factor
  • After this loop, f[num] contains all prime factors of num

Step 3: Union Numbers with Common Prime Factors

def find(x):
    if p[x] != x:
        p[x] = find(p[x])
    return p[x]

for i in nums:
    for j in f[i]:
        p[find(i)] = find(j)

The find function implements path compression for efficient lookups. For each number in nums, we union it with all its prime factors. This cleverly connects all numbers sharing any prime factor:

  • If numbers A and B both have prime factor P, both will be unioned with P
  • This puts A and B in the same component

Step 4: Verify Sortability

s = sorted(nums)
for i, num in enumerate(nums):
    if s[i] != num and find(num) != find(s[i]):
        return False
return True

We create the sorted version of the array. Then for each position:

  • If the current number nums[i] differs from what should be there s[i]
  • We check if they're in the same connected component using find
  • If they're not connected (can't be swapped), return False
  • If all positions can be satisfied, return True

The time complexity is O(n log n + m log m) where n is the array length and m is the maximum value, due to sorting and the sieve. The space complexity is O(m) for the Union-Find structure and prime factor storage.

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 the solution with the array nums = [6, 10, 15].

Step 1: Initialize Union-Find We create a parent array where each element points to itself initially:

  • p[6] = 6, p[10] = 10, p[15] = 15, etc.

Step 2: Find Prime Factors Using the sieve approach, we find prime factors for each number up to 15:

  • f[6] = [2, 3] (6 = 2 × 3)
  • f[10] = [2, 5] (10 = 2 × 5)
  • f[15] = [3, 5] (15 = 3 × 5)

Step 3: Union Numbers with Their Prime Factors For each number, we union it with its prime factors:

For 6 with prime factors [2, 3]:

  • Union 6 with 2: p[6] = 2
  • Union 6 with 3: p[2] = 3 (since find(6) = 2)

For 10 with prime factors [2, 5]:

  • Union 10 with 2: Since find(2) = 3, we set p[10] = 3
  • Union 10 with 5: p[3] = 5 (connecting the whole component)

For 15 with prime factors [3, 5]:

  • Union 15 with 3: Since find(3) = 5, we set p[15] = 5
  • Union 15 with 5: Already connected

Now all numbers 6, 10, and 15 are in the same component (all eventually point to 5).

Step 4: Check if Sorting is Possible

  • Original: [6, 10, 15]
  • Sorted: [6, 10, 15] (already sorted!)
  • Position 0: nums[0] = 6, sorted[0] = 6 - same number, OK
  • Position 1: nums[1] = 10, sorted[1] = 10 - same number, OK
  • Position 2: nums[2] = 15, sorted[2] = 15 - same number, OK

Return true - the array can be sorted (it's already sorted).

Let's try a different example where swapping is needed: nums = [10, 6, 15]

Using the same Union-Find structure from above:

  • Original: [10, 6, 15]
  • Sorted: [6, 10, 15]
  • Position 0: nums[0] = 10, sorted[0] = 6
    • find(10) = 5, find(6) = 5 - same component, can swap ✓
  • Position 1: nums[1] = 6, sorted[1] = 10
    • find(6) = 5, find(10) = 5 - same component, can swap ✓
  • Position 2: nums[2] = 15, sorted[2] = 15 - same number, OK

Return true - the array can be sorted through valid swaps.

For a negative example, consider nums = [3, 7]:

  • f[3] = [3] (3 is prime)
  • f[7] = [7] (7 is prime)
  • After unions: 3 and 7 remain in separate components
  • Original: [3, 7], Sorted: [3, 7] - already sorted, returns true

But if we had nums = [7, 3]:

  • Original: [7, 3], Sorted: [3, 7]
  • Position 0: nums[0] = 7, sorted[0] = 3
    • find(7) = 7, find(3) = 3 - different components, cannot swap ✗
  • Return false - cannot sort this array

Solution Implementation

1class Solution:
2    def gcdSort(self, nums: List[int]) -> bool:
3        # Initialize Union-Find parent array for all possible values
4        max_value = 10**5 + 10
5        parent = list(range(max_value))
6      
7        # Dictionary to store prime factors for each number
8        prime_factors = defaultdict(list)
9      
10        # Find the maximum value in nums to limit our factorization
11        max_num = max(nums)
12      
13        # Sieve-like approach to find prime factors for all numbers up to max_num
14        for prime_candidate in range(2, max_num + 1):
15            # Skip if we've already found factors for this number
16            if prime_factors[prime_candidate]:
17                continue
18          
19            # Mark all multiples of this prime
20            for multiple in range(prime_candidate, max_num + 1, prime_candidate):
21                prime_factors[multiple].append(prime_candidate)
22      
23        # Union-Find find function with path compression
24        def find(x):
25            if parent[x] != x:
26                parent[x] = find(parent[x])  # Path compression
27            return parent[x]
28      
29        # Union all numbers that share prime factors
30        for number in nums:
31            # Connect this number with all its prime factors
32            for prime_factor in prime_factors[number]:
33                parent[find(number)] = find(prime_factor)
34      
35        # Check if the array can be sorted
36        sorted_nums = sorted(nums)
37      
38        # Verify each position: if elements differ, they must be in the same component
39        for index, original_num in enumerate(nums):
40            target_num = sorted_nums[index]
41            # If numbers are different and not in the same connected component, cannot sort
42            if target_num != original_num and find(original_num) != find(target_num):
43                return False
44      
45        return True
46
1class Solution {
2    private int[] parent;
3  
4    public boolean gcdSort(int[] nums) {
5        // Initialize Union-Find structure with maximum possible value
6        int maxValue = 100010;
7        parent = new int[maxValue];
8      
9        // Initialize each element as its own parent (self-representative)
10        for (int i = 0; i < maxValue; i++) {
11            parent[i] = i;
12        }
13      
14        // Map to store prime factors for each number
15        Map<Integer, List<Integer>> primeFactorsMap = new HashMap<>();
16      
17        // Find the maximum value in the array to limit our computation
18        int maxNum = 0;
19        for (int num : nums) {
20            maxNum = Math.max(maxNum, num);
21        }
22      
23        // Sieve-like approach to find prime factors for all numbers up to maxNum
24        for (int prime = 2; prime <= maxNum; prime++) {
25            // Skip if we've already processed this number as a composite
26            if (primeFactorsMap.containsKey(prime)) {
27                continue;
28            }
29          
30            // Mark all multiples of this prime and add prime to their factor list
31            for (int multiple = prime; multiple <= maxNum; multiple += prime) {
32                primeFactorsMap.computeIfAbsent(multiple, k -> new ArrayList<>()).add(prime);
33            }
34        }
35      
36        // Union numbers that share common prime factors
37        for (int num : nums) {
38            List<Integer> primeFactors = primeFactorsMap.get(num);
39            if (primeFactors != null) {
40                for (int primeFactor : primeFactors) {
41                    // Union the number with its prime factor
42                    parent[find(num)] = find(primeFactor);
43                }
44            }
45        }
46      
47        // Create a sorted copy of the original array
48        int[] sortedArray = new int[nums.length];
49        System.arraycopy(nums, 0, sortedArray, 0, nums.length);
50        Arrays.sort(sortedArray);
51      
52        // Check if elements that need to be swapped are in the same connected component
53        for (int i = 0; i < nums.length; i++) {
54            // If element is not in its sorted position and not connected to its target position
55            if (sortedArray[i] != nums[i] && find(nums[i]) != find(sortedArray[i])) {
56                return false;
57            }
58        }
59      
60        return true;
61    }
62  
63    // Find operation with path compression for Union-Find
64    int find(int x) {
65        if (parent[x] != x) {
66            // Path compression: make all nodes point directly to root
67            parent[x] = find(parent[x]);
68        }
69        return parent[x];
70    }
71}
72
1class Solution {
2public:
3    vector<int> parent;
4  
5    bool gcdSort(vector<int>& nums) {
6        // Initialize Union-Find structure with size 100010
7        int maxSize = 100010;
8        parent.resize(maxSize);
9        for (int i = 0; i < maxSize; ++i) {
10            parent[i] = i;
11        }
12      
13        // Find the maximum element in nums
14        int maxElement = 0;
15        for (int num : nums) {
16            maxElement = max(maxElement, num);
17        }
18      
19        // Build prime factorization map using Sieve of Eratosthenes approach
20        // primeFactors[i] contains all prime factors of number i
21        unordered_map<int, vector<int>> primeFactors;
22        for (int i = 2; i <= maxElement; ++i) {
23            // Skip if already processed (not a prime)
24            if (!primeFactors[i].empty()) {
25                continue;
26            }
27            // Mark all multiples of i with i as a prime factor
28            for (int j = i; j <= maxElement; j += i) {
29                primeFactors[j].push_back(i);
30            }
31        }
32      
33        // Union numbers that share common prime factors
34        // Numbers with GCD > 1 will be in the same connected component
35        for (int num : nums) {
36            for (int primeFactor : primeFactors[num]) {
37                // Union the number with its prime factors
38                parent[find(num)] = find(primeFactor);
39            }
40        }
41      
42        // Create sorted version of the original array
43        vector<int> sortedNums = nums;
44        sort(sortedNums.begin(), sortedNums.end());
45      
46        // Check if each element can reach its sorted position
47        // Elements can be swapped if they're in the same connected component
48        for (int i = 0; i < nums.size(); ++i) {
49            if (sortedNums[i] != nums[i] && find(sortedNums[i]) != find(nums[i])) {
50                // Element needs to move but can't reach target position
51                return false;
52            }
53        }
54      
55        return true;
56    }
57  
58    // Find operation with path compression for Union-Find
59    int find(int x) {
60        if (parent[x] != x) {
61            parent[x] = find(parent[x]);  // Path compression
62        }
63        return parent[x];
64    }
65};
66
1let parent: number[];
2
3function gcdSort(nums: number[]): boolean {
4    // Initialize Union-Find structure with size 100010
5    const maxSize = 100010;
6    parent = new Array(maxSize);
7    for (let i = 0; i < maxSize; i++) {
8        parent[i] = i;
9    }
10  
11    // Find the maximum element in nums
12    let maxElement = 0;
13    for (const num of nums) {
14        maxElement = Math.max(maxElement, num);
15    }
16  
17    // Build prime factorization map using Sieve of Eratosthenes approach
18    // primeFactors[i] contains all prime factors of number i
19    const primeFactors: Map<number, number[]> = new Map();
20    for (let i = 2; i <= maxElement; i++) {
21        // Skip if already processed (not a prime)
22        if (primeFactors.has(i)) {
23            continue;
24        }
25        // Mark all multiples of i with i as a prime factor
26        for (let j = i; j <= maxElement; j += i) {
27            if (!primeFactors.has(j)) {
28                primeFactors.set(j, []);
29            }
30            primeFactors.get(j)!.push(i);
31        }
32    }
33  
34    // Union numbers that share common prime factors
35    // Numbers with GCD > 1 will be in the same connected component
36    for (const num of nums) {
37        const factors = primeFactors.get(num) || [];
38        for (const primeFactor of factors) {
39            // Union the number with its prime factors
40            parent[find(num)] = find(primeFactor);
41        }
42    }
43  
44    // Create sorted version of the original array
45    const sortedNums = [...nums].sort((a, b) => a - b);
46  
47    // Check if each element can reach its sorted position
48    // Elements can be swapped if they're in the same connected component
49    for (let i = 0; i < nums.length; i++) {
50        if (sortedNums[i] !== nums[i] && find(sortedNums[i]) !== find(nums[i])) {
51            // Element needs to move but can't reach target position
52            return false;
53        }
54    }
55  
56    return true;
57}
58
59// Find operation with path compression for Union-Find
60function find(x: number): number {
61    if (parent[x] !== x) {
62        parent[x] = find(parent[x]); // Path compression
63    }
64    return parent[x];
65}
66

Time and Space Complexity

Time Complexity: O(n * log(log(n)) + m * α(n) * f + m * log(m))

Where:

  • n is the maximum value in nums (used as the upper bound for sieve)
  • m is the length of the nums array
  • α(n) is the inverse Ackermann function (practically constant)
  • f is the average number of prime factors per number

Breaking down the operations:

  1. Prime factorization using sieve: O(n * log(log(n))) - The nested loops iterate through all numbers up to mx and mark their factors, which follows a harmonic series pattern similar to the Sieve of Eratosthenes
  2. Union operations: O(m * f * α(n)) - For each number in nums, we perform union operations for all its prime factors
  3. Sorting: O(m * log(m)) - Standard sorting of the nums array
  4. Final verification: O(m * α(n)) - For each element, we perform find operations

The dominant term is typically O(n * log(log(n))) when n is large relative to m.

Space Complexity: O(n + m)

Breaking down the space usage:

  1. Parent array p: O(n) - stores parent pointers for values up to 10^5 + 10
  2. Factor dictionary f: O(n * log(n)) in worst case - each number can have at most O(log(n)) prime factors
  3. Sorted array s: O(m) - copy of the input array

The dominant term is O(n) due to the parent array initialization.

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

Common Pitfalls

1. Inefficient Prime Factorization

A common mistake is using naive factorization for each number individually, which leads to timeout for large inputs.

Incorrect approach:

def get_prime_factors(n):
    factors = []
    for i in range(2, int(n**0.5) + 1):
        while n % i == 0:
            factors.append(i)
            n //= i
    if n > 1:
        factors.append(n)
    return factors

# Then calling this for each number - O(n * sqrt(m))
for num in nums:
    factors = get_prime_factors(num)
    # ... union operations

Why it fails: This approach has O(n * sqrt(m)) complexity for factorization alone, where n is array length and m is the maximum value. For arrays with many large numbers, this becomes prohibitively slow.

Solution: Use the sieve-based approach shown in the main solution, which pre-computes all prime factors in O(m log m) time once, rather than factorizing each number separately.

2. Incorrect Union-Find Implementation

Forgetting path compression or implementing union incorrectly can cause wrong results or poor performance.

Common mistakes:

# Mistake 1: No path compression
def find(x):
    while parent[x] != x:
        x = parent[x]
    return x

# Mistake 2: Direct assignment without find
for num in nums:
    for prime in prime_factors[num]:
        parent[num] = prime  # Wrong! Should use find()

Solution: Always use path compression and ensure both elements are reduced to their roots before union:

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

# Correct union
parent[find(num)] = find(prime)

3. Memory Limit Issues with Large Range

Creating arrays for the full range up to 10^5 when the actual maximum is much smaller wastes memory.

Better approach:

# Dynamically size based on actual maximum
max_num = max(nums)
parent = list(range(max_num + 1))

4. Missing Edge Cases

Not handling special cases like arrays with duplicate numbers or single elements.

Solution: The algorithm naturally handles these cases, but be aware:

  • Duplicate numbers will always be in the same component (they share all prime factors)
  • Single element arrays always return True
  • Arrays already sorted return True (each element is compared with itself)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

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

Load More