2584. Split the Array to Make Coprime Products
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
toi
(inclusive) - The second part contains elements from index
i + 1
ton - 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. Sincegcd(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. Sincegcd(6, 3) = 3 ≠ 1
, this split is not valid. - Split at index
i = 2
is not allowed sincei
must be less thann - 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
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 factorlast
: A list wherelast[i]
will store the rightmost index that must be grouped with indexi
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 withnums[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 EvaluatorExample 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)
- Prime factor 2:
-
Index 1:
nums[1] = 7
- Prime factor 7:
first[7] = 1
,last[1] = 1
(initially)
- Prime factor 7:
-
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)
- Prime factor 3:
-
Index 3:
nums[3] = 8 = 2³
- Prime factor 2: Already in
first
at index 0, so updatelast[0] = 3
- Prime factor 2: Already in
-
Index 4:
nums[4] = 3
- Prime factor 3: Already in
first
at index 2, so updatelast[2] = 4
- Prime factor 3: Already in
-
Index 5:
nums[5] = 5
- Prime factor 5: Already in
first
at index 2, so updatelast[2] = 5
- Prime factor 5: Already in
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 3last[1] = 1
: Prime factor 7 only appears at index 1last[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 ifmx (3) < i (1)
? No. Updatemx = max(3, last[1]) = max(3, 1) = 3
i = 2
: Check ifmx (3) < i (2)
? No. Updatemx = max(3, last[2]) = max(3, 5) = 5
i = 3
: Check ifmx (5) < i (3)
? No. Updatemx = max(5, last[3]) = max(5, 0) = 5
i = 4
: Check ifmx (5) < i (4)
? No. Updatemx = max(5, last[4]) = max(5, 0) = 5
i = 5
: Check ifmx (5) < i (5)
? No. Updatemx = 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 ifmx (0) < i (1)
? Yes! Returnmx = 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
toj ≤ √x
, which isO(√x)
iterations - Inside this loop, we check if
j
dividesx
and perform dictionary operations (O(1) average case) - The inner while loop
while x % j == 0
divides out all factors ofj
, but this doesn't increase the overall complexity as the total number of divisions across all prime factors isO(log x)
- The while loop runs from
- After the factorization loop, we handle the case where
x > 1
(whenx
is a prime factor greater than√x
), which isO(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 mostO(p)
entries - The
last
array storesn
integers, requiringO(n)
space - Since
p ≤ n * log m
(each number has at mostO(log m)
prime factors), and in practicep
is often much smaller thann
, the space complexity isO(n + p) = O(n)
in the worst case - However, more precisely, the space complexity is
O(n)
for thelast
array plusO(p)
for the dictionary, giving usO(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
Which of these properties could exist for a graph but not a tree?
Recommended Readings
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!