2436. Minimum Split Into Subarrays With GCD Greater Than One đź”’
Problem Description
You have an array nums
containing only positive integers. Your task is to split this array into the minimum number of contiguous subarrays where each subarray must satisfy a specific condition about its GCD (Greatest Common Divisor).
The splitting rules are:
- Every element from the original array must belong to exactly one subarray
- For each subarray you create, the GCD of all elements in that subarray must be strictly greater than
1
- The subarrays must be contiguous (you cannot rearrange elements)
The GCD of a subarray is the largest positive integer that divides all elements in that subarray evenly. For example, the GCD of [6, 12, 18]
is 6
, while the GCD of [4, 6, 8]
is 2
.
Your goal is to find the minimum number of such subarrays needed to partition the entire array while satisfying the GCD constraint.
For example, if you have an array [12, 6, 3, 14, 8]
, you might split it into [12, 6, 3]
(GCD = 3) and [14, 8]
(GCD = 2), resulting in 2 subarrays. Each subarray has a GCD greater than 1, and you've used the minimum number of splits possible.
Intuition
The key insight is to be greedy and try to extend each subarray as long as possible while maintaining a GCD greater than 1.
Think about it this way: when we're building a subarray from left to right, we want to include as many elements as possible in the current subarray before we're forced to start a new one. The only time we're forced to start a new subarray is when adding the next element would make the GCD become 1.
Why does this greedy approach work? Because if we can maintain a GCD greater than 1 by including more elements, there's no benefit to splitting earlier - we'd just create more subarrays unnecessarily.
The mathematical property we leverage is that the GCD of a set of numbers can only decrease or stay the same as we add more numbers - it never increases. Starting with any number, its GCD with 0 is itself (by convention, gcd(0, x) = x
). As we add more numbers, we compute gcd(current_gcd, new_number)
.
The critical moment is when the running GCD becomes 1. At this point, no matter what additional numbers we add, the GCD will remain 1 (since 1 divides everything). This violates our constraint, so we must start a new subarray at this point.
The algorithm naturally emerges: maintain a running GCD for the current subarray. When it becomes 1, we know we've gone too far - the current element cannot be part of the previous subarray. So we increment our subarray count and start fresh with the current element as the beginning of a new subarray.
This greedy strategy ensures we use the minimum number of subarrays because we only split when absolutely necessary - when continuing would violate the GCD > 1 constraint.
Learn more about Greedy, Math and Dynamic Programming patterns.
Solution Approach
The implementation follows a straightforward greedy traversal of the array while maintaining the running GCD of the current subarray.
We initialize two variables:
ans = 1
: The answer counter starting at 1 (since we need at least one subarray)g = 0
: The running GCD of the current subarray, initialized to 0
The choice of initializing g = 0
is clever because of the mathematical property that gcd(0, x) = x
for any positive integer x
. This means when we encounter the first element, the GCD will simply become that element.
For each element x
in the array:
- Calculate the new GCD:
g = gcd(g, x)
- Check if the GCD has become 1:
- If
g == 1
, this means we cannot includex
in the current subarray (as it would violate the GCD > 1 constraint) - Start a new subarray: increment
ans += 1
- Reset the GCD for the new subarray:
g = x
(the current element becomes the first element of the new subarray)
- If
- If
g != 1
, we can safely includex
in the current subarray and continue
The algorithm processes each element exactly once in a single pass, making it very efficient.
Let's trace through an example with nums = [6, 12, 3, 14, 8]
:
- Start:
ans = 1
,g = 0
- Element 6:
g = gcd(0, 6) = 6
, continue - Element 12:
g = gcd(6, 12) = 6
, continue - Element 3:
g = gcd(6, 3) = 3
, continue - Element 14:
g = gcd(3, 14) = 1
, split needed!ans = 2
,g = 14
- Element 8:
g = gcd(14, 8) = 2
, continue - Result: 2 subarrays
[6, 12, 3]
and[14, 8]
The time complexity is O(n Ă— log(max(nums)))
where the log factor comes from the GCD calculation, and space complexity is O(1)
as we only use a constant amount of extra space.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with the array nums = [12, 6, 3, 14, 8]
.
Initial Setup:
ans = 1
(we need at least one subarray)g = 0
(running GCD, starts at 0)
Step 1: Process element 12
- Calculate:
g = gcd(0, 12) = 12
- Since
g = 12 > 1
, we can include 12 in the current subarray - Current subarray:
[12]
Step 2: Process element 6
- Calculate:
g = gcd(12, 6) = 6
- Since
g = 6 > 1
, we can include 6 in the current subarray - Current subarray:
[12, 6]
Step 3: Process element 3
- Calculate:
g = gcd(6, 3) = 3
- Since
g = 3 > 1
, we can include 3 in the current subarray - Current subarray:
[12, 6, 3]
Step 4: Process element 14
- Calculate:
g = gcd(3, 14) = 1
- Since
g = 1
, we cannot include 14 in the current subarray! - We must start a new subarray:
- Increment:
ans = 2
- Reset:
g = 14
(14 becomes the first element of the new subarray)
- Increment:
- First subarray complete:
[12, 6, 3]
with GCD = 3 - New current subarray:
[14]
Step 5: Process element 8
- Calculate:
g = gcd(14, 8) = 2
- Since
g = 2 > 1
, we can include 8 in the current subarray - Current subarray:
[14, 8]
Final Result:
- Total subarrays needed:
ans = 2
- The array splits into:
[12, 6, 3]
(GCD = 3) and[14, 8]
(GCD = 2) - Both subarrays satisfy the constraint of having GCD > 1
The greedy approach successfully minimizes the number of subarrays by only splitting when absolutely necessary (when the GCD would become 1).
Solution Implementation
1from typing import List
2from math import gcd
3
4class Solution:
5 def minimumSplits(self, nums: List[int]) -> int:
6 # Initialize the number of splits needed (starting with 1 for the first subarray)
7 num_splits = 1
8
9 # Track the running GCD of the current subarray
10 current_gcd = 0
11
12 # Iterate through each number in the array
13 for num in nums:
14 # Calculate GCD of current subarray including this number
15 # Note: gcd(0, x) = x for any x, which handles initialization
16 current_gcd = gcd(current_gcd, num)
17
18 # If GCD becomes 1, we cannot continue this subarray
19 # (since we need GCD > 1 for valid subarrays)
20 if current_gcd == 1:
21 # Start a new subarray
22 num_splits += 1
23 # Reset GCD to current number for the new subarray
24 current_gcd = num
25
26 return num_splits
27
1class Solution {
2 /**
3 * Finds the minimum number of splits needed to partition the array
4 * such that each subarray has a GCD greater than 1.
5 *
6 * @param nums the input array of integers
7 * @return the minimum number of splits (subarrays) required
8 */
9 public int minimumSplits(int[] nums) {
10 int splitCount = 1; // Initialize with 1 split (at least one subarray exists)
11 int currentGcd = 0; // Running GCD of current subarray (GCD with 0 returns the other number)
12
13 // Iterate through each element in the array
14 for (int currentNum : nums) {
15 // Calculate GCD of current subarray including this element
16 currentGcd = gcd(currentGcd, currentNum);
17
18 // If GCD becomes 1, we need to start a new subarray
19 if (currentGcd == 1) {
20 splitCount++; // Increment split count
21 currentGcd = currentNum; // Start new subarray with current element
22 }
23 }
24
25 return splitCount;
26 }
27
28 /**
29 * Calculates the Greatest Common Divisor (GCD) of two numbers
30 * using Euclidean algorithm.
31 *
32 * @param a first integer
33 * @param b second integer
34 * @return the GCD of a and b
35 */
36 private int gcd(int a, int b) {
37 // Base case: when b is 0, return a
38 // Recursive case: GCD(a, b) = GCD(b, a % b)
39 return b == 0 ? a : gcd(b, a % b);
40 }
41}
42
1class Solution {
2public:
3 int minimumSplits(vector<int>& nums) {
4 // Initialize the number of splits (starting with 1 as we need at least one subarray)
5 int splitCount = 1;
6
7 // Track the running GCD of the current subarray
8 int currentGCD = 0;
9
10 // Iterate through each number in the array
11 for (int num : nums) {
12 // Update the GCD by including the current number
13 currentGCD = gcd(currentGCD, num);
14
15 // If GCD becomes 1, we need to start a new subarray
16 if (currentGCD == 1) {
17 // Increment the split count
18 ++splitCount;
19
20 // Reset GCD to the current number (start of new subarray)
21 currentGCD = num;
22 }
23 }
24
25 return splitCount;
26 }
27};
28
1/**
2 * Calculates the minimum number of splits needed in an array
3 * where each split segment must have GCD greater than 1
4 * @param nums - The input array of numbers
5 * @returns The minimum number of splits required
6 */
7function minimumSplits(nums: number[]): number {
8 // Initialize the answer with 1 (at least one segment)
9 let answer: number = 1;
10
11 // Track the running GCD of the current segment
12 let currentGcd: number = 0;
13
14 // Iterate through each number in the array
15 for (const currentNumber of nums) {
16 // Calculate GCD of current segment including this number
17 currentGcd = gcd(currentGcd, currentNumber);
18
19 // If GCD becomes 1, we need to start a new segment
20 if (currentGcd === 1) {
21 // Increment the split count
22 answer++;
23 // Start new segment with current number
24 currentGcd = currentNumber;
25 }
26 }
27
28 return answer;
29}
30
31/**
32 * Calculates the Greatest Common Divisor (GCD) of two numbers
33 * using Euclidean algorithm
34 * @param a - First number
35 * @param b - Second number
36 * @returns The GCD of a and b
37 */
38function gcd(a: number, b: number): number {
39 // Recursive implementation: if b is 0, return a; otherwise continue
40 return b ? gcd(b, a % b) : a;
41}
42
Time and Space Complexity
The time complexity is O(n Ă— log m)
, where n
is the length of the array and m
is the maximum value in the array. This is because:
- The algorithm iterates through the array once, which takes
O(n)
time - For each element, it computes the GCD using the
gcd(g, x)
function - The GCD computation between two numbers has a time complexity of
O(log m)
wherem
is the maximum of the two numbers being compared - Therefore, the overall time complexity is
O(n Ă— log m)
The space complexity is O(1)
as the algorithm only uses a constant amount of extra space regardless of the input size. The variables ans
, g
, and x
are the only additional storage used, and they don't scale with the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrectly Handling Arrays with All Elements Having GCD = 1
A critical edge case occurs when the entire array consists of numbers that are pairwise coprime (their GCD is 1). For example, consider nums = [2, 3, 5, 7]
where all elements are prime numbers.
The Problem:
- When processing element 2:
g = gcd(0, 2) = 2
- When processing element 3:
g = gcd(2, 3) = 1
→ Split!ans = 2
,g = 3
- When processing element 5:
g = gcd(3, 5) = 1
→ Split!ans = 3
,g = 5
- When processing element 7:
g = gcd(5, 7) = 1
→ Split!ans = 4
,g = 7
This results in 4 subarrays: [2]
, [3]
, [5]
, [7]
. However, each single-element subarray has a GCD equal to itself. If any element equals 1, that subarray would violate the GCD > 1 constraint!
Solution: Before running the algorithm, check if any element in the array equals 1. If so, return -1 or handle it as an impossible case:
def minimumSplits(self, nums: List[int]) -> int:
# Check for impossible case
if 1 in nums:
return -1 # Cannot form valid subarrays if any element is 1
num_splits = 1
current_gcd = 0
for num in nums:
current_gcd = gcd(current_gcd, num)
if current_gcd == 1:
num_splits += 1
current_gcd = num
return num_splits
Pitfall 2: Misunderstanding GCD Initialization with Zero
Developers might be confused about why current_gcd
is initialized to 0 instead of the first element, potentially leading to incorrect implementations like:
# Incorrect approach
current_gcd = nums[0] # Wrong initialization
for i in range(1, len(nums)): # Skipping first element
current_gcd = gcd(current_gcd, nums[i])
# ... rest of logic
The Problem: This approach requires special handling for empty arrays or single-element arrays and makes the code more complex.
Solution:
Stick with current_gcd = 0
initialization. The mathematical property gcd(0, x) = x
elegantly handles the first element without special cases. This makes the code cleaner and works uniformly for all array sizes:
current_gcd = 0 # Correct initialization for num in nums: # Process ALL elements uniformly current_gcd = gcd(current_gcd, num) # ... rest of logic
Pitfall 3: Off-by-One Error in Answer Initialization
Some might initialize the answer counter to 0 instead of 1, thinking in terms of "number of splits" rather than "number of subarrays":
# Incorrect num_splits = 0 # Wrong! This counts split points, not subarrays
The Problem: If you start with 0, you're counting the number of times you split, not the number of resulting subarrays. An array split k times results in k+1 subarrays.
Solution: Always initialize to 1 since you start with at least one subarray:
num_splits = 1 # Correct! We start with one subarray
Alternatively, if you prefer counting split points, remember to add 1 to the final result:
split_points = 0 # ... algorithm logic incrementing split_points return split_points + 1 # Convert splits to number of subarrays
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!