Facebook Pixel

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:

  1. For each subarray, the greatest common divisor (GCD) between its first element and last element must be greater than 1
  2. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Base Case: When i >= n (we've processed all elements), return 0 as no more subarrays are needed.

  2. Recursive Case: For the current position i, try all possible endpoints j where i ≤ j < n:

    • Check if gcd(nums[i], nums[j]) > 1
    • If true, this forms a valid subarray from i to j
    • 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
  3. Initialize: Start the recursion from index 0 with dfs(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 to inf (infinity) to handle cases where no valid splitting exists
  • For each starting position i, we enumerate all possible ending positions j
  • 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 Evaluator

Example 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 most n different values of i (from 0 to n-1)
  • Due to the @cache decorator, each unique value of i is computed only once
  • For each call to dfs(i), the inner loop iterates from i to n-1, which takes O(n) time in the worst case
  • Inside the loop, we compute gcd(nums[i], nums[j]) which takes O(log(min(nums[i], nums[j]))) time, but this is typically considered O(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 most n 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).

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which type of traversal does breadth first search do?


Recommended Readings

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

Load More