Taro Logo

Greatest Common Divisor Traversal

Hard
Google logo
Google
Topics:
ArraysGraphsGreedy Algorithms

You are given a 0-indexed integer array nums, and you are allowed to traverse between its indices. You can traverse between index i and index j, i != j, if and only if gcd(nums[i], nums[j]) > 1, where gcd is the greatest common divisor.

Your task is to determine if for every pair of indices i and j in nums, where i < j, there exists a sequence of traversals that can take us from i to j.

Return true if it is possible to traverse between all such pairs of indices, or false otherwise.

Example 1:

Input: nums = [2,3,6]
Output: true
Explanation:
In this example, there are 3 possible pairs of indices: (0, 1), (0, 2), and (1, 2).
To go from index 0 to index 1, we can use the sequence of traversals 0 -> 2 -> 1, where we move from index 0 to index 2 because gcd(nums[0], nums[2]) = gcd(2, 6) = 2 > 1, and then move from index 2 to index 1 because gcd(nums[2], nums[1]) = gcd(6, 3) = 3 > 1.
To go from index 0 to index 2, we can just go directly because gcd(nums[0], nums[2]) = gcd(2, 6) = 2 > 1. Likewise, to go from index 1 to index 2, we can just go directly because gcd(nums[1], nums[2]) = gcd(3, 6) = 3 > 1.

Example 2:

Input: nums = [3,9,5]
Output: false
Explanation: No sequence of traversals can take us from index 0 to index 2 in this example. So, we return false.

Example 3:

Input: nums = [4,3,12,8]
Output: true
Explanation:
There are 6 possible pairs of indices to traverse between: (0, 1), (0, 2), (0, 3), (1, 2), (1, 3), and (2, 3). A valid sequence of traversals exists for each pair, so we return true.

Constraints:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^5

Solution


Naive Solution

A brute force solution would involve checking every possible path between each pair of indices (i, j) to see if a valid traversal exists. This would likely involve a Depth-First Search (DFS) or Breadth-First Search (BFS) for each pair.

Algorithm:

  1. For each pair of indices (i, j) where i < j:
  2. Perform a DFS or BFS starting from index i to find a path to index j.
  3. In the DFS/BFS, only traverse from index u to index v if gcd(nums[u], nums[v]) > 1.
  4. If no path is found for any pair, return false.
  5. If paths are found for all pairs, return true.

Code (Python):

import math

def can_traverse_naive(nums):
    def gcd(a, b):
        if b == 0:
            return a
        return gcd(b, a % b)

    def has_path(start, end, visited):
        if start == end:
            return True
        
        visited[start] = True
        
        for i in range(len(nums)):
            if not visited[i] and gcd(nums[start], nums[i]) > 1:
                if has_path(i, end, visited):
                    return True
        
        return False

    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            visited = [False] * len(nums)
            if not has_path(i, j, visited):
                return False
    
    return True

Time Complexity:

O(N^2 * (N + E)), where N is the number of elements in nums and E is the number of edges (pairs with GCD > 1). In the worst case, for each of the N^2 pairs, we might have to explore all N nodes and E edges.

Space Complexity:

O(N) due to the recursion depth of DFS and the visited array.

Optimal Solution: Union-Find

A more efficient solution uses the Union-Find (Disjoint Set Union) data structure. The idea is to connect indices i and j if gcd(nums[i], nums[j]) > 1. After processing all such pairs, we check if all indices are connected, meaning there's a path between any two indices.

Algorithm:

  1. Create a Union-Find data structure with n elements, where n is the length of nums.
  2. Iterate through each pair of indices (i, j) where i < j.
  3. If gcd(nums[i], nums[j]) > 1, union indices i and j in the Union-Find.
  4. After processing all pairs, check if all elements belong to the same connected component. This can be done by checking if find(0) == find(i) for all i from 1 to n-1.
  5. Return true if all elements are in the same component, and false otherwise.

However, calculating GCD for all pairs of indices is still O(N^2) in the worst case. Instead, we can iterate through each number, find its prime factors, and union the indices that share a prime factor. This dramatically reduces the runtime complexity.

  1. Create a Union-Find data structure with n elements, where n is the length of nums.
  2. Create a Map<Integer, Integer> to store the first occurrence index for each prime factor.
  3. Iterate through each number nums[i]:
    • Find the prime factors of nums[i].
    • For each prime factor:
      • If the prime factor is not in the Map, add it to the Map and associate with the index i.
      • If the prime factor exists in the Map, union i with the index stored in the Map.
  4. After processing all indices, check if all elements belong to the same connected component.

Code (Python):

import math

def can_traverse(nums):
    n = len(nums)
    parent = list(range(n))

    def find(i):
        if parent[i] == i:
            return i
        parent[i] = find(parent[i])
        return parent[i]

    def union(i, j):
        root_i = find(i)
        root_j = find(j)
        if root_i != root_j:
            parent[root_i] = root_j

    prime_map = {}

    def prime_factors(n):
        factors = set()
        while n % 2 == 0:
            factors.add(2)
            n //= 2
        for i in range(3, int(math.sqrt(n)) + 1, 2):
            while n % i == 0:
                factors.add(i)
                n //= i
        if n > 2:
            factors.add(n)
        return factors

    for i in range(n):
        factors = prime_factors(nums[i])
        for factor in factors:
            if factor in prime_map:
                union(i, prime_map[factor])
            else:
                prime_map[factor] = i

    # Check if all nodes are connected
    root = find(0)
    for i in range(1, n):
        if find(i) != root:
            return False

    return True

Time Complexity:

O(N * sqrt(M) + N * α(N)), where N is the number of elements in nums, M is the maximum value in nums, and α(N) is the inverse Ackermann function, which grows very slowly and can be considered almost constant for practical input sizes. O(N * sqrt(M)) is for finding prime factors of all numbers and O(N * α(N)) is for Union-Find operations.

Space Complexity:

O(N + P), where N is the number of elements in nums (for the parent array in Union-Find) and P is the number of distinct prime factors among all numbers in nums (for the prime_map).

Edge Cases:

  • Single Element: If nums contains only one element, return true because there are no pairs to traverse.
  • Elements equal to 1: Elements equal to 1 do not share GCD > 1 with other elements. If no other elements share a GCD > 1 return false.
  • Large Prime Numbers: The prime_factors function efficiently finds prime factors, but very large prime numbers could increase the execution time.