2464. Minimum Subarrays in a Valid Split đ
Problem Description
You are given an integer array nums
. Your task is to split this array into subarrays such that the splitting satisfies specific conditions.
A valid splitting must meet two requirements:
- For each subarray, the greatest common divisor (GCD) between its first element and last element must be greater than 1
- Every element in the original array must belong to exactly one subarray (no overlapping, no missing elements)
You need to find the minimum number of subarrays needed to create a valid splitting. If it's impossible to create a valid splitting, return -1
.
Key definitions:
- The greatest common divisor (GCD) of two numbers is the largest positive integer that divides both numbers evenly
- A subarray is a contiguous sequence of elements from the array (cannot be empty)
For example, if you have a subarray [6, 10, 3]
, you would check if GCD(6, 3) > 1
. Since GCD(6, 3) = 3
, which is greater than 1, this subarray satisfies the GCD condition.
The solution uses dynamic programming with memoization. The function dfs(i)
calculates the minimum number of subarrays needed starting from index i
. For each position i
, it tries all possible endpoints j
where i ⤠j < n
. If GCD(nums[i], nums[j]) > 1
, it can form a valid subarray from i
to j
, and then recursively solve for the remaining array starting from j + 1
. The algorithm returns the minimum number of subarrays found, or -1
if no valid splitting exists.
Intuition
The key insight is that we need to make decisions about where to end each subarray, and these decisions affect what options are available for the remaining elements. This naturally suggests a dynamic programming approach.
Think about it this way: when we're at position i
, we need to decide where this subarray should end. We could end it at position i
itself (making a subarray of length 1), or at i+1
, i+2
, and so on. But not all endpoints are valid - we can only choose endpoint j
if GCD(nums[i], nums[j]) > 1
.
Why check only the first and last elements? Because the problem specifically requires that the GCD of the first and last elements of each subarray be greater than 1. This is the constraint that determines whether a subarray is valid.
Once we choose an endpoint j
for the current subarray (from i
to j
), we've used up elements from index i
to j
. Now we face the exact same problem starting from index j+1
. This recursive structure is perfect for dynamic programming.
The recurrence relation becomes clear: the minimum number of subarrays starting from position i
equals 1 (for the current subarray) plus the minimum number of subarrays needed for the rest of the array starting from j+1
, where we try all valid values of j
.
We use memoization to avoid recalculating the same subproblems. If we've already computed the minimum number of subarrays starting from a particular index, we can reuse that result instead of computing it again.
The base case is when we've processed all elements (i >= n
), which means we need 0 more subarrays. If we can't find any valid splitting from a position, we return infinity to indicate it's impossible, and ultimately return -1
if the minimum is still infinity.
Learn more about Math and Dynamic Programming patterns.
Solution Approach
The solution implements a memoization search (top-down dynamic programming) approach to find the minimum number of valid subarrays.
Main Function Structure:
The core of the solution is the dfs(i)
function which represents the minimum number of subarrays needed starting from index i
. The @cache
decorator is used for memoization to store previously computed results.
Algorithm Steps:
-
Base Case: When
i >= n
(we've processed all elements), return0
as no more subarrays are needed. -
Recursive Case: For the current position
i
, try all possible endpointsj
wherei ⤠j < n
:- Check if
gcd(nums[i], nums[j]) > 1
- If true, this forms a valid subarray from
i
toj
- Calculate the cost as
1 + dfs(j + 1)
(1 for current subarray plus the minimum for the remaining array) - Track the minimum cost across all valid endpoints
- Check if
-
Initialize: Start the recursion from index
0
withdfs(0)
Implementation Details:
@cache
def dfs(i):
if i >= n:
return 0
ans = inf
for j in range(i, n):
if gcd(nums[i], nums[j]) > 1:
ans = min(ans, 1 + dfs(j + 1))
return ans
- The variable
ans
is initialized toinf
(infinity) to handle cases where no valid splitting exists - For each starting position
i
, we enumerate all possible ending positionsj
- The
gcd(nums[i], nums[j])
function (from Python's math library) checks if the first and last elements of the subarray satisfy the GCD condition - We use
min()
to keep track of the optimal (minimum) number of subarrays
Final Result:
After computing dfs(0)
, we check if the result is less than infinity. If it is, we return the result; otherwise, we return -1
to indicate no valid splitting exists.
The dfs.cache_clear()
is called to clean up the memoization cache after computation.
Time Complexity: O(n²)
for trying all possible subarray endpoints, with each GCD computation taking O(log(max(nums)))
time.
Space Complexity: O(n)
for the memoization cache and recursion stack.
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 = [6, 3, 9, 2, 4]
.
We start with dfs(0)
, trying to find the minimum number of subarrays starting from index 0.
From index 0 (element 6):
- Try endpoint j=0: subarray [6], GCD(6,6) = 6 > 1 â
- Cost: 1 + dfs(1)
- Try endpoint j=1: subarray [6,3], GCD(6,3) = 3 > 1 â
- Cost: 1 + dfs(2)
- Try endpoint j=2: subarray [6,3,9], GCD(6,9) = 3 > 1 â
- Cost: 1 + dfs(3)
- Try endpoint j=3: subarray [6,3,9,2], GCD(6,2) = 2 > 1 â
- Cost: 1 + dfs(4)
- Try endpoint j=4: subarray [6,3,9,2,4], GCD(6,4) = 2 > 1 â
- Cost: 1 + dfs(5)
Let's explore one path: choosing j=1 (subarray [6,3]).
From index 2 (element 9):
- Try endpoint j=2: subarray [9], GCD(9,9) = 9 > 1 â
- Cost: 1 + dfs(3)
- Try endpoint j=3: subarray [9,2], GCD(9,2) = 1 â (invalid)
- Try endpoint j=4: subarray [9,2,4], GCD(9,4) = 1 â (invalid)
So from index 2, we must take the single element [9], giving us cost 1 + dfs(3).
From index 3 (element 2):
- Try endpoint j=3: subarray [2], GCD(2,2) = 2 > 1 â
- Cost: 1 + dfs(4)
- Try endpoint j=4: subarray [2,4], GCD(2,4) = 2 > 1 â
- Cost: 1 + dfs(5)
Let's choose j=4 (subarray [2,4]).
From index 5: This is beyond the array length, so dfs(5) returns 0 (base case).
Backtracking the costs:
- dfs(5) = 0
- dfs(3) = 1 + dfs(5) = 1 + 0 = 1 (using subarray [2,4])
- dfs(2) = 1 + dfs(3) = 1 + 1 = 2 (using subarray [9])
- dfs(0) considers 1 + dfs(2) = 1 + 2 = 3 (using subarray [6,3])
The algorithm tries all possibilities and finds the minimum. In this case, the optimal solution is 3 subarrays: [6,3], [9], [2,4].
Solution Implementation
1class Solution:
2 def validSubarraySplit(self, nums: List[int]) -> int:
3 """
4 Find the minimum number of subarrays to split the array such that
5 each subarray has at least one pair of elements with GCD > 1.
6
7 Args:
8 nums: List of integers to split
9
10 Returns:
11 Minimum number of subarrays, or -1 if impossible
12 """
13 from functools import cache
14 from math import gcd, inf
15 from typing import List
16
17 @cache
18 def dfs(start_index: int) -> float:
19 """
20 Dynamic programming function to find minimum splits from current position.
21
22 Args:
23 start_index: Current starting position in the array
24
25 Returns:
26 Minimum number of subarrays needed from this position
27 """
28 # Base case: reached end of array
29 if start_index >= array_length:
30 return 0
31
32 # Initialize minimum splits to infinity
33 min_splits = inf
34
35 # Try all possible ending positions for current subarray
36 for end_index in range(start_index, array_length):
37 # Check if current element and end element have GCD > 1
38 if gcd(nums[start_index], nums[end_index]) > 1:
39 # Recursively calculate splits for remaining array
40 current_splits = 1 + dfs(end_index + 1)
41 min_splits = min(min_splits, current_splits)
42
43 return min_splits
44
45 # Initialize array length
46 array_length = len(nums)
47
48 # Calculate minimum splits starting from index 0
49 result = dfs(0)
50
51 # Clear the cache after computation
52 dfs.cache_clear()
53
54 # Return result if valid, otherwise return -1
55 return result if result < inf else -1
56
1class Solution {
2 private int arrayLength;
3 private int[] memoization;
4 private int[] inputArray;
5 private static final int INFINITY = 0x3f3f3f3f;
6
7 /**
8 * Finds the minimum number of valid subarrays to split the input array.
9 * A valid subarray has GCD(first element, last element) > 1.
10 *
11 * @param nums the input array to split
12 * @return minimum number of splits, or -1 if impossible
13 */
14 public int validSubarraySplit(int[] nums) {
15 arrayLength = nums.length;
16 memoization = new int[arrayLength];
17 this.inputArray = nums;
18
19 int minSplits = findMinimumSplits(0);
20 return minSplits < INFINITY ? minSplits : -1;
21 }
22
23 /**
24 * Recursively finds the minimum number of splits starting from index startIndex.
25 * Uses dynamic programming with memoization to optimize.
26 *
27 * @param startIndex the starting index for current subarray
28 * @return minimum number of subarrays needed from startIndex to end
29 */
30 private int findMinimumSplits(int startIndex) {
31 // Base case: reached beyond array bounds
32 if (startIndex >= arrayLength) {
33 return 0;
34 }
35
36 // Return memoized result if already computed
37 if (memoization[startIndex] > 0) {
38 return memoization[startIndex];
39 }
40
41 int minimumResult = INFINITY;
42
43 // Try all possible ending positions for current subarray
44 for (int endIndex = startIndex; endIndex < arrayLength; ++endIndex) {
45 // Check if subarray [startIndex, endIndex] is valid
46 // Valid means GCD of first and last elements > 1
47 if (calculateGCD(inputArray[startIndex], inputArray[endIndex]) > 1) {
48 // Recursively solve for remaining array and add 1 for current subarray
49 minimumResult = Math.min(minimumResult, 1 + findMinimumSplits(endIndex + 1));
50 }
51 }
52
53 // Store result in memoization array
54 memoization[startIndex] = minimumResult;
55 return minimumResult;
56 }
57
58 /**
59 * Calculates the Greatest Common Divisor of two numbers using Euclidean algorithm.
60 *
61 * @param a first number
62 * @param b second number
63 * @return GCD of a and b
64 */
65 private int calculateGCD(int a, int b) {
66 return b == 0 ? a : calculateGCD(b, a % b);
67 }
68}
69
1class Solution {
2public:
3 const int INF = 0x3f3f3f3f; // Large value representing infinity
4
5 int validSubarraySplit(vector<int>& nums) {
6 int n = nums.size();
7
8 // dp[i] stores the minimum number of subarrays needed to split nums[i..n-1]
9 vector<int> dp(n, 0);
10
11 // Recursive function with memoization to find minimum splits
12 // Returns the minimum number of subarrays needed starting from index i
13 function<int(int)> dfs = [&](int i) -> int {
14 // Base case: if we've processed all elements, no more subarrays needed
15 if (i >= n) {
16 return 0;
17 }
18
19 // Return memoized result if already computed
20 if (dp[i] != 0) {
21 return dp[i];
22 }
23
24 int minSplits = INF;
25
26 // Try all possible ending positions for the current subarray
27 for (int j = i; j < n; ++j) {
28 // Check if nums[i] and nums[j] have GCD > 1
29 // This ensures all elements in subarray [i, j] can form a valid subarray
30 if (__gcd(nums[i], nums[j]) > 1) {
31 // If valid, consider this split and recurse for remaining elements
32 minSplits = min(minSplits, 1 + dfs(j + 1));
33 }
34 }
35
36 // Memoize and return the result
37 dp[i] = minSplits;
38 return minSplits;
39 };
40
41 // Start the recursion from index 0
42 int result = dfs(0);
43
44 // If no valid split exists, return -1; otherwise return the minimum splits
45 return result < INF ? result : -1;
46 }
47};
48
1const INF = 0x3f3f3f3f; // Large value representing infinity
2
3function validSubarraySplit(nums: number[]): number {
4 const n = nums.length;
5
6 // dp[i] stores the minimum number of subarrays needed to split nums[i..n-1]
7 const dp: number[] = new Array(n).fill(0);
8
9 /**
10 * Calculate the greatest common divisor of two numbers
11 * @param a - First number
12 * @param b - Second number
13 * @returns The GCD of a and b
14 */
15 const gcd = (a: number, b: number): number => {
16 while (b !== 0) {
17 const temp = b;
18 b = a % b;
19 a = temp;
20 }
21 return a;
22 };
23
24 /**
25 * Recursive function with memoization to find minimum splits
26 * @param i - Starting index for the current subarray
27 * @returns The minimum number of subarrays needed starting from index i
28 */
29 const dfs = (i: number): number => {
30 // Base case: if we've processed all elements, no more subarrays needed
31 if (i >= n) {
32 return 0;
33 }
34
35 // Return memoized result if already computed
36 if (dp[i] !== 0) {
37 return dp[i];
38 }
39
40 let minSplits = INF;
41
42 // Try all possible ending positions for the current subarray
43 for (let j = i; j < n; j++) {
44 // Check if nums[i] and nums[j] have GCD > 1
45 // This ensures all elements in subarray [i, j] can form a valid subarray
46 if (gcd(nums[i], nums[j]) > 1) {
47 // If valid, consider this split and recurse for remaining elements
48 minSplits = Math.min(minSplits, 1 + dfs(j + 1));
49 }
50 }
51
52 // Memoize and return the result
53 dp[i] = minSplits;
54 return minSplits;
55 };
56
57 // Start the recursion from index 0
58 const result = dfs(0);
59
60 // If no valid split exists, return -1; otherwise return the minimum splits
61 return result < INF ? result : -1;
62}
63
Time and Space Complexity
Time Complexity: O(n^2)
The analysis breaks down as follows:
- The function
dfs(i)
can be called with at mostn
different values ofi
(from 0 to n-1) - Due to the
@cache
decorator, each unique value ofi
is computed only once - For each call to
dfs(i)
, the inner loop iterates fromi
ton-1
, which takesO(n)
time in the worst case - Inside the loop, we compute
gcd(nums[i], nums[j])
which takesO(log(min(nums[i], nums[j])))
time, but this is typically consideredO(1)
for practical integer ranges - Therefore, the total time complexity is
O(n) * O(n) = O(n^2)
Space Complexity: O(n)
The space usage consists of:
- The recursion stack depth can go up to
O(n)
in the worst case (when we process each element sequentially) - The
@cache
decorator stores at mostn
different results (one for each possible starting position from 0 to n-1) - Each cached result stores a single integer value
- Therefore, the total space complexity is
O(n)
for both the cache storage and the recursion stack
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the GCD Condition
A common mistake is thinking that ALL pairs within a subarray need to have GCD > 1, when actually only the FIRST and LAST elements of each subarray need to satisfy this condition.
Incorrect interpretation:
# Wrong: Checking all pairs in the subarray
for k in range(i, j+1):
for l in range(k+1, j+1):
if gcd(nums[k], nums[l]) <= 1:
valid = False
Correct approach:
# Right: Only check first and last elements if gcd(nums[i], nums[j]) > 1: # Valid subarray from i to j
2. Single Element Subarray Edge Case
When i == j
(single element subarray), we need gcd(nums[i], nums[i]) > 1
, which equals nums[i]
. This means single-element subarrays are only valid if the element itself is greater than 1.
Problem scenario:
nums = [1, 2, 3] # The element 1 cannot form a valid single-element subarray
Solution: The algorithm handles this correctly by checking gcd(nums[i], nums[j])
even when i == j
, which returns nums[i]
.
3. Memory Issues with Large Arrays
The @cache
decorator stores all computed results, which can cause memory issues for very large arrays.
Solution: Add cache clearing or use iterative DP:
# Alternative: Bottom-up DP to control memory usage
def validSubarraySplit(self, nums: List[int]) -> int:
n = len(nums)
dp = [float('inf')] * (n + 1)
dp[n] = 0
for i in range(n - 1, -1, -1):
for j in range(i, n):
if gcd(nums[i], nums[j]) > 1:
dp[i] = min(dp[i], 1 + dp[j + 1])
return dp[0] if dp[0] < float('inf') else -1
4. Forgetting to Handle Impossible Cases
Not properly returning -1
when no valid splitting exists.
Incorrect:
result = dfs(0) return result # May return inf instead of -1
Correct:
result = dfs(0) return result if result < inf else -1
5. Off-by-One Errors in Index Handling
Confusion about whether to use dfs(j)
or dfs(j + 1)
after forming a subarray ending at index j
.
Key insight: After using elements from index i
to j
inclusive, the next subarray should start at j + 1
, so we call dfs(j + 1)
.
Which type of traversal does breadth first search do?
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
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
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
Want a Structured Path to Master System Design Too? Donât Miss This!