Facebook Pixel

2584. Split the Array to Make Coprime Products

HardArrayHash TableMathNumber Theory
Leetcode Link

Problem Description

You are given a 0-indexed integer array nums of length n. Your task is to find the smallest valid split index in the array.

A split at index i (where 0 <= i <= n - 2) divides the array into two parts:

  • The first part contains elements from index 0 to i (inclusive)
  • The second part contains elements from index i + 1 to n - 1 (inclusive)

A split is considered valid if the product of all elements in the first part and the product of all elements in the second part are coprime (their greatest common divisor equals 1).

For example, with nums = [2, 3, 3]:

  • Split at index i = 0: First part = [2] with product 2, second part = [3, 3] with product 9. Since gcd(2, 9) = 1, this split is valid.
  • Split at index i = 1: First part = [2, 3] with product 6, second part = [3] with product 3. Since gcd(6, 3) = 3 ≠ 1, this split is not valid.
  • Split at index i = 2 is not allowed since i must be less than n - 1.

The goal is to return the smallest index i where a valid split can be made. If no valid split exists, return -1.

Key Points:

  • The split must divide the array into two non-empty parts
  • Two numbers are coprime if their greatest common divisor (GCD) is 1
  • You need to find the leftmost (smallest index) valid split position
  • The products can be very large, but you don't need to compute them directly - you can work with prime factors instead
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that two products are coprime if and only if they share no common prime factors. Instead of computing potentially huge products, we can track which prime factors appear on each side of the split.

Think about it this way: if a prime factor p appears in any number on the left side and also appears in any number on the right side, then both products will be divisible by p, making their GCD at least p (not coprime).

This leads to an important observation: for any prime factor p, all numbers containing p must be on the same side of the split. If we have numbers at indices i₁, i₂, ..., iₖ that all contain prime factor p, then any valid split must either place all of them on the left side or all on the right side.

We can reformulate this as an interval problem: for each prime factor p, find the first index where it appears (first[p]) and the last index where it appears (last[p]). All occurrences of p form an interval [first[p], last[p]] that cannot be split.

The problem now becomes: find the smallest index i such that no prime factor interval crosses the boundary between index i and i+1. In other words, for every prime factor, either all its occurrences are at indices <= i or all are at indices > i.

We can solve this greedily: starting from index 0, we keep track of the maximum right boundary of all prime factor intervals that start at or before our current position. If we ever reach a position i where this maximum boundary equals i, we've found a valid split point - all prime factors that appeared so far have their last occurrence at or before i.

The algorithm essentially merges overlapping intervals of prime factors and finds the first gap between merged intervals, which corresponds to our valid split point.

Learn more about Math patterns.

Solution Approach

The implementation follows our intuition of tracking prime factor intervals:

Step 1: Find Prime Factors and Track Their Intervals

We initialize two data structures:

  • first: A dictionary to store the first occurrence index of each prime factor
  • last: A list where last[i] will store the rightmost index that must be grouped with index i

For each number in the array, we factorize it to find all its prime factors:

for i, x in enumerate(nums):
    j = 2
    while j <= x // j:  # Check divisors up to sqrt(x)
        if x % j == 0:
            if j in first:
                last[first[j]] = i  # Update the last occurrence for this prime
            else:
                first[j] = i  # Record first occurrence of this prime
            while x % j == 0:
                x //= j  # Remove all occurrences of this prime factor
        j += 1
    if x > 1:  # x is now a prime factor itself
        if x in first:
            last[first[x]] = i
        else:
            first[x] = i

This process ensures that for each prime factor p, we know its first occurrence at first[p] and update last[first[p]] to be the rightmost index containing p.

Step 2: Find the Earliest Valid Split

We traverse the array while maintaining mx, the maximum value of last[j] for all indices j we've seen so far:

mx = last[0]
for i, x in enumerate(last):
    if mx < i:
        return mx  # Found a valid split at index mx
    mx = max(mx, x)
return -1  # No valid split found

The key insight: if mx < i, it means all prime factors that appeared at or before index mx have their last occurrence at or before mx. This makes mx a valid split point because no prime factor crosses the boundary between mx and mx + 1.

Why This Works:

  • last[i] represents the furthest index that shares at least one prime factor with nums[i]
  • By maintaining the maximum of these values as we iterate, mx represents the rightmost boundary of all prime factors seen so far
  • When mx < i, we've found a gap where we can split without any prime factor appearing on both sides
  • We return the first such mx found, giving us the smallest valid split index

Time Complexity: O(n * sqrt(M)) where n is the array length and M is the maximum value in the array (for factorization).

Space Complexity: O(n + P) where P is the number of distinct prime factors across all numbers.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with nums = [4, 7, 15, 8, 3, 5].

Step 1: Find prime factors for each number and track intervals

We'll build our first dictionary and last array:

  • Index 0: nums[0] = 4 = 2²

    • Prime factor 2: first[2] = 0, last[0] = 0 (initially)
  • Index 1: nums[1] = 7

    • Prime factor 7: first[7] = 1, last[1] = 1 (initially)
  • Index 2: nums[2] = 15 = 3 × 5

    • Prime factor 3: first[3] = 2, last[2] = 2 (initially)
    • Prime factor 5: first[5] = 2, last[2] = 2 (stays same)
  • Index 3: nums[3] = 8 = 2³

    • Prime factor 2: Already in first at index 0, so update last[0] = 3
  • Index 4: nums[4] = 3

    • Prime factor 3: Already in first at index 2, so update last[2] = 4
  • Index 5: nums[5] = 5

    • Prime factor 5: Already in first at index 2, so update last[2] = 5

Final state:

  • first = {2: 0, 7: 1, 3: 2, 5: 2}
  • last = [3, 1, 5, 0, 0, 0]

The last array tells us:

  • last[0] = 3: Prime factor 2 appears from index 0 to 3
  • last[1] = 1: Prime factor 7 only appears at index 1
  • last[2] = 5: Prime factors 3 and 5 appear from index 2 to 5

Step 2: Find the earliest valid split

Now we traverse with mx tracking the maximum right boundary:

  • i = 0: mx = last[0] = 3
  • i = 1: Check if mx (3) < i (1)? No. Update mx = max(3, last[1]) = max(3, 1) = 3
  • i = 2: Check if mx (3) < i (2)? No. Update mx = max(3, last[2]) = max(3, 5) = 5
  • i = 3: Check if mx (5) < i (3)? No. Update mx = max(5, last[3]) = max(5, 0) = 5
  • i = 4: Check if mx (5) < i (4)? No. Update mx = max(5, last[4]) = max(5, 0) = 5
  • i = 5: Check if mx (5) < i (5)? No. Update mx = max(5, last[5]) = max(5, 0) = 5

We never find a position where mx < i, so return -1.

This makes sense because:

  • Prime 2 appears at indices 0 and 3 (spans 0-3)
  • Primes 3 and 5 appear at indices 2, 4, and 5 (spans 2-5)
  • These intervals overlap at indices 2 and 3, meaning we can't split anywhere without having at least one prime factor on both sides

Let's verify with a different example where a valid split exists: nums = [2, 3, 5]

  • Index 0: nums[0] = 2, first[2] = 0, last[0] = 0
  • Index 1: nums[1] = 3, first[3] = 1, last[1] = 1
  • Index 2: nums[2] = 5, first[5] = 2, last[2] = 2

Final: last = [0, 1, 2]

Traversal:

  • i = 0: mx = last[0] = 0
  • i = 1: Check if mx (0) < i (1)? Yes! Return mx = 0

Split at index 0: left = [2] (product 2), right = [3, 5] (product 15). Since gcd(2, 15) = 1, this is valid.

Solution Implementation

1class Solution:
2    def findValidSplit(self, nums: List[int]) -> int:
3        # Dictionary to store the first occurrence index of each prime factor
4        first_occurrence = {}
5        n = len(nums)
6        # List to store the rightmost index that shares a prime factor with index i
7        rightmost_shared = list(range(n))
8      
9        # Process each number to find its prime factors
10        for index, number in enumerate(nums):
11            current_num = number
12            divisor = 2
13          
14            # Find all prime factors using trial division
15            while divisor * divisor <= current_num:
16                if current_num % divisor == 0:
17                    # Found a prime factor
18                    if divisor in first_occurrence:
19                        # Update the rightmost index for the first occurrence of this prime
20                        rightmost_shared[first_occurrence[divisor]] = index
21                    else:
22                        # Record the first occurrence of this prime factor
23                        first_occurrence[divisor] = index
24                  
25                    # Remove all occurrences of this prime factor
26                    while current_num % divisor == 0:
27                        current_num //= divisor
28              
29                divisor += 1
30          
31            # Handle the case where current_num is a prime number > sqrt(original)
32            if current_num > 1:
33                if current_num in first_occurrence:
34                    # Update the rightmost index for the first occurrence of this prime
35                    rightmost_shared[first_occurrence[current_num]] = index
36                else:
37                    # Record the first occurrence of this prime factor
38                    first_occurrence[current_num] = index
39      
40        # Find the valid split point by checking if we can split before reaching
41        # the maximum rightmost index encountered so far
42        max_rightmost = rightmost_shared[0]
43      
44        for current_index, current_rightmost in enumerate(rightmost_shared):
45            # If max_rightmost is less than current index, we found a valid split
46            if max_rightmost < current_index:
47                return max_rightmost
48            # Update the maximum rightmost index seen so far
49            max_rightmost = max(max_rightmost, current_rightmost)
50      
51        # No valid split found
52        return -1
53
1class Solution {
2    public int findValidSplit(int[] nums) {
3        // Map to store the first occurrence index of each prime factor
4        Map<Integer, Integer> firstOccurrence = new HashMap<>();
5        int n = nums.length;
6      
7        // Array to store the last occurrence index that must be included 
8        // if we include element at index i in the left part
9        int[] lastRequired = new int[n];
10      
11        // Initialize each element's last required index as itself
12        for (int i = 0; i < n; ++i) {
13            lastRequired[i] = i;
14        }
15      
16        // Process each number to find prime factors and update dependencies
17        for (int i = 0; i < n; ++i) {
18            int currentNum = nums[i];
19          
20            // Find all prime factors of currentNum
21            // Check divisors up to sqrt(currentNum)
22            for (int divisor = 2; divisor * divisor <= currentNum; ++divisor) {
23                if (currentNum % divisor == 0) {
24                    // Found a prime factor
25                    if (firstOccurrence.containsKey(divisor)) {
26                        // Update the last required index for the first occurrence
27                        // to include current index (they share a common factor)
28                        lastRequired[firstOccurrence.get(divisor)] = i;
29                    } else {
30                        // Record the first occurrence of this prime factor
31                        firstOccurrence.put(divisor, i);
32                    }
33                  
34                    // Remove all occurrences of this prime factor
35                    while (currentNum % divisor == 0) {
36                        currentNum /= divisor;
37                    }
38                }
39            }
40          
41            // Handle remaining prime factor (if currentNum > 1 after factorization)
42            if (currentNum > 1) {
43                if (firstOccurrence.containsKey(currentNum)) {
44                    // Update the last required index for the first occurrence
45                    lastRequired[firstOccurrence.get(currentNum)] = i;
46                } else {
47                    // Record the first occurrence of this prime factor
48                    firstOccurrence.put(currentNum, i);
49                }
50            }
51        }
52      
53        // Find the earliest valid split point
54        // Track the maximum last required index we've seen so far
55        int maxLastRequired = lastRequired[0];
56      
57        for (int i = 0; i < n; ++i) {
58            // If current index exceeds the maximum last required index,
59            // we can split at the previous position
60            if (maxLastRequired < i) {
61                return maxLastRequired;
62            }
63            // Update the maximum last required index
64            maxLastRequired = Math.max(maxLastRequired, lastRequired[i]);
65        }
66      
67        // No valid split found
68        return -1;
69    }
70}
71
1class Solution {
2public:
3    int findValidSplit(vector<int>& nums) {
4        // Map to store the first occurrence index of each prime factor
5        unordered_map<int, int> firstOccurrence;
6      
7        int n = nums.size();
8      
9        // Array to store the last index that must be included if we start from index i
10        // Initially, each element points to itself
11        vector<int> lastRequired(n);
12        iota(lastRequired.begin(), lastRequired.end(), 0);
13      
14        // Process each number to find prime factors and update required ranges
15        for (int i = 0; i < n; ++i) {
16            int currentNum = nums[i];
17          
18            // Find all prime factors using trial division
19            // Check divisors up to sqrt(currentNum)
20            for (int divisor = 2; divisor * divisor <= currentNum; ++divisor) {
21                if (currentNum % divisor == 0) {
22                    // Found a prime factor
23                    if (firstOccurrence.count(divisor)) {
24                        // This prime factor appeared before, update the last required index
25                        lastRequired[firstOccurrence[divisor]] = i;
26                    } else {
27                        // First occurrence of this prime factor
28                        firstOccurrence[divisor] = i;
29                    }
30                  
31                    // Remove all occurrences of this prime factor
32                    while (currentNum % divisor == 0) {
33                        currentNum /= divisor;
34                    }
35                }
36            }
37          
38            // If currentNum > 1, it's a prime factor itself
39            if (currentNum > 1) {
40                if (firstOccurrence.count(currentNum)) {
41                    // This prime factor appeared before, update the last required index
42                    lastRequired[firstOccurrence[currentNum]] = i;
43                } else {
44                    // First occurrence of this prime factor
45                    firstOccurrence[currentNum] = i;
46                }
47            }
48        }
49      
50        // Find the valid split point by tracking the maximum required index
51        int maxRequiredIndex = lastRequired[0];
52      
53        for (int i = 0; i < n; ++i) {
54            // If current maximum required index is less than current position,
55            // we found a valid split point
56            if (maxRequiredIndex < i) {
57                return maxRequiredIndex;
58            }
59          
60            // Update the maximum required index
61            maxRequiredIndex = max(maxRequiredIndex, lastRequired[i]);
62        }
63      
64        // No valid split found
65        return -1;
66    }
67};
68
1function findValidSplit(nums: number[]): number {
2    // Map to store the first occurrence index of each prime factor
3    const firstOccurrence: Map<number, number> = new Map();
4  
5    const n = nums.length;
6  
7    // Array to store the last index that must be included if we start from index i
8    // Initially, each element points to itself
9    const lastRequired: number[] = Array.from({ length: n }, (_, i) => i);
10  
11    // Process each number to find prime factors and update required ranges
12    for (let i = 0; i < n; i++) {
13        let currentNum = nums[i];
14      
15        // Find all prime factors using trial division
16        // Check divisors up to sqrt(currentNum)
17        for (let divisor = 2; divisor * divisor <= currentNum; divisor++) {
18            if (currentNum % divisor === 0) {
19                // Found a prime factor
20                if (firstOccurrence.has(divisor)) {
21                    // This prime factor appeared before, update the last required index
22                    lastRequired[firstOccurrence.get(divisor)!] = i;
23                } else {
24                    // First occurrence of this prime factor
25                    firstOccurrence.set(divisor, i);
26                }
27              
28                // Remove all occurrences of this prime factor
29                while (currentNum % divisor === 0) {
30                    currentNum = Math.floor(currentNum / divisor);
31                }
32            }
33        }
34      
35        // If currentNum > 1, it's a prime factor itself
36        if (currentNum > 1) {
37            if (firstOccurrence.has(currentNum)) {
38                // This prime factor appeared before, update the last required index
39                lastRequired[firstOccurrence.get(currentNum)!] = i;
40            } else {
41                // First occurrence of this prime factor
42                firstOccurrence.set(currentNum, i);
43            }
44        }
45    }
46  
47    // Find the valid split point by tracking the maximum required index
48    let maxRequiredIndex = lastRequired[0];
49  
50    for (let i = 0; i < n; i++) {
51        // If current maximum required index is less than current position,
52        // we found a valid split point
53        if (maxRequiredIndex < i) {
54            return maxRequiredIndex;
55        }
56      
57        // Update the maximum required index
58        maxRequiredIndex = Math.max(maxRequiredIndex, lastRequired[i]);
59    }
60  
61    // No valid split found
62    return -1;
63}
64

Time and Space Complexity

Time Complexity: O(n * √m) where n is the length of the array and m is the maximum value in the array.

  • The outer loop iterates through all n elements in the array
  • For each element x, we perform prime factorization:
    • The while loop runs from j = 2 to j ≤ √x, which is O(√x) iterations
    • Inside this loop, we check if j divides x and perform dictionary operations (O(1) average case)
    • The inner while loop while x % j == 0 divides out all factors of j, but this doesn't increase the overall complexity as the total number of divisions across all prime factors is O(log x)
  • After the factorization loop, we handle the case where x > 1 (when x is a prime factor greater than √x), which is O(1)
  • The final loop to find the valid split point iterates through the array once, which is O(n)
  • Overall: O(n * √m + n) = O(n * √m)

Space Complexity: O(p) where p is the number of distinct prime factors across all elements in the array.

  • The first dictionary stores the first occurrence index of each prime factor, containing at most O(p) entries
  • The last array stores n integers, requiring O(n) space
  • Since p ≤ n * log m (each number has at most O(log m) prime factors), and in practice p is often much smaller than n, the space complexity is O(n + p) = O(n) in the worst case
  • However, more precisely, the space complexity is O(n) for the last array plus O(p) for the dictionary, giving us O(n + p)

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

Common Pitfalls

1. Inefficient Prime Factorization Leading to TLE

One of the most common pitfalls is using an inefficient prime factorization method. Many developers initially try checking all numbers up to n-1 as potential divisors, or use while divisor <= current_num instead of while divisor * divisor <= current_num.

Problem Code:

# Inefficient - checks too many divisors
while divisor <= current_num:  # Wrong!
    if current_num % divisor == 0:
        # process prime factor

Solution: Only check divisors up to the square root of the number. Any factor larger than the square root must be paired with a factor smaller than the square root.

while divisor * divisor <= current_num:  # Correct!
    if current_num % divisor == 0:
        # process prime factor

2. Computing Actual Products (Integer Overflow)

Another pitfall is attempting to compute the actual products of array segments and then calculating their GCD. This fails because:

  • Products can become astronomically large (exceeding even 64-bit integers)
  • Computing GCD of very large numbers is computationally expensive

Problem Code:

# Wrong approach - will overflow or be very slow
left_product = 1
right_product = 1
for i in range(split_index + 1):
    left_product *= nums[i]
for i in range(split_index + 1, n):
    right_product *= nums[i]
return gcd(left_product, right_product) == 1

Solution: Work with prime factors instead of actual products. Track which indices share prime factors rather than computing products.

3. Incorrect Handling of Prime Factors Greater Than sqrt(n)

After the factorization loop, if the remaining number is greater than 1, it's a prime factor itself. Forgetting to handle this case will miss important prime factors.

Problem Code:

while divisor * divisor <= current_num:
    # ... handle divisors ...
    divisor += 1
# Forgot to check if current_num > 1!

Solution: Always check if the remaining number after factorization is greater than 1:

while divisor * divisor <= current_num:
    # ... handle divisors ...
    divisor += 1

if current_num > 1:  # Don't forget this!
    # current_num is itself a prime factor
    if current_num in first_occurrence:
        rightmost_shared[first_occurrence[current_num]] = index
    else:
        first_occurrence[current_num] = index

4. Off-by-One Errors in Split Index

It's easy to confuse whether we're returning the split index itself or the index where we detected the split is valid. The problem asks for the split index (where the first part ends), not the detection point.

Problem Code:

for current_index, current_rightmost in enumerate(rightmost_shared):
    if max_rightmost < current_index:
        return current_index  # Wrong! This returns the detection point

Solution: Return max_rightmost which represents the actual split position:

for current_index, current_rightmost in enumerate(rightmost_shared):
    if max_rightmost < current_index:
        return max_rightmost  # Correct! This is the split index

5. Not Removing All Occurrences of a Prime Factor

When a prime factor is found, all its occurrences must be removed from the number. Using a single division instead of a while loop will leave duplicate prime factors.

Problem Code:

if current_num % divisor == 0:
    # Record the prime factor
    current_num //= divisor  # Wrong! Only removes one occurrence

Solution: Use a while loop to remove all occurrences:

if current_num % divisor == 0:
    # Record the prime factor
    while current_num % divisor == 0:  # Correct!
        current_num //= divisor
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

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

Load More