Facebook Pixel

2369. Check if There is a Valid Partition For The Array

Problem Description

You are given a 0-indexed integer array nums. Your task is to determine if you can partition this array into one or more contiguous subarrays where each subarray must satisfy exactly one of the following conditions:

  1. Two equal elements: The subarray contains exactly 2 elements that are the same. For example, [2,2] is valid.

  2. Three equal elements: The subarray contains exactly 3 elements that are all the same. For example, [4,4,4] is valid.

  3. Three consecutive increasing elements: The subarray contains exactly 3 elements where each element is exactly 1 more than the previous element. For example, [3,4,5] is valid (since 4-3=1 and 5-4=1), but [1,3,5] is not valid.

The partition must use all elements in the array, and the subarrays must be contiguous (you cannot skip elements or rearrange them).

Return true if at least one valid partition exists for the entire array, otherwise return false.

For example:

  • nums = [4,4,4,5,6] can be partitioned as [4,4,4] and [5,6] (but [5,6] doesn't satisfy any condition, so this would return false)
  • nums = [1,1,1,2] can be partitioned as [1,1,1] and [2] (but a single element doesn't satisfy any condition, so this would return false)
  • nums = [1,1,2,2,3,3] can be partitioned as [1,1], [2,2], [3,3] (all subarrays satisfy condition 1, so this would return true)
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 a series of decisions about how to partition the array. At each position, we need to decide: should we form a 2-element subarray, a 3-element subarray, or is it even possible to form a valid subarray starting from this position?

Think of it like walking through the array from left to right. At each step, we have limited choices - we can only "consume" the next 2 or 3 elements if they form a valid pattern. Once we make a choice, we move forward and face the same decision at the new position.

This naturally leads to a recursive approach: at position i, we check what valid subarrays we can form starting from i, and for each valid option, we recursively check if the remaining array (starting from where our current subarray ends) can also be validly partitioned.

The problem exhibits optimal substructure - if we can validly partition the array from position i onwards, it doesn't matter how we got to position i. This means we'll likely encounter the same subproblem multiple times. For instance, different partitioning choices in the beginning might lead us to check "can we partition from index 5 onwards?" multiple times.

This overlap in subproblems suggests using dynamic programming or memoization. We can cache the result for each starting position i - once we know whether the array from position i onwards can be validly partitioned, we don't need to recalculate it.

The base case is straightforward: if we've successfully consumed all elements (reached or passed the end of the array), we've found a valid partition. If we're stuck at a position where no valid subarray can be formed, that path fails. The answer is whether starting from position 0, we can find at least one way to validly partition the entire array.

Learn more about Dynamic Programming patterns.

Solution Approach

We implement a recursive function dfs(i) that determines whether a valid partition exists starting from index i. The function uses memoization with @cache decorator to avoid recalculating the same subproblems.

Base Case:

  • If i >= n (we've reached or passed the end of the array), return True as we've successfully partitioned all elements.

Recursive Cases: At each position i, we check three possible valid subarray patterns:

  1. Two equal elements (a): Check if i+1 < n and nums[i] == nums[i+1]. If true, we can form a 2-element subarray and recursively check dfs(i+2).

  2. Three equal elements (b): Check if i+2 < n and nums[i] == nums[i+1] == nums[i+2]. If true, we can form a 3-element subarray and recursively check dfs(i+3).

  3. Three consecutive increasing elements (c): Check if i+2 < n, nums[i+1] - nums[i] == 1, and nums[i+2] - nums[i+1] == 1. If true, we can form a 3-element consecutive subarray and recursively check dfs(i+3).

Decision Logic: The function returns True if:

  • Pattern a is valid AND dfs(i+2) returns True, OR
  • Either pattern b or c is valid AND dfs(i+3) returns True

This can be expressed as:

dfs(i) = (a and dfs(i+2)) or ((b or c) and dfs(i+3))

Implementation Details:

  • We store the array length n for boundary checking
  • The @cache decorator automatically memoizes results, preventing redundant calculations
  • We start the recursion with dfs(0) to check if the entire array can be validly partitioned

Time Complexity: O(n) where n is the length of the array, since each position is computed at most once due to memoization.

Space Complexity: O(n) for the recursion stack and memoization cache.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through the example nums = [1,1,2,3,4] to illustrate how the solution works.

Initial Setup:

  • Array: [1,1,2,3,4], length n = 5
  • We start with dfs(0) to check if we can partition from index 0

Step 1: dfs(0) - Processing index 0 (value = 1)

  • Check pattern a (two equal): nums[0] == nums[1]? → 1 == 1 ✓ Valid
  • Check pattern b (three equal): nums[0] == nums[1] == nums[2]? → 1 == 1 == 2 ✗ Invalid
  • Check pattern c (three consecutive): nums[1]-nums[0] == 1 and nums[2]-nums[1] == 1? → 1-1 == 1 ✗ Invalid
  • Since pattern a is valid, we check: dfs(2)

Step 2: dfs(2) - Processing index 2 (value = 2)

  • Check pattern a (two equal): nums[2] == nums[3]? → 2 == 3 ✗ Invalid
  • Check pattern b (three equal): nums[2] == nums[3] == nums[4]? → 2 == 3 == 4 ✗ Invalid
  • Check pattern c (three consecutive): nums[3]-nums[2] == 1 and nums[4]-nums[3] == 1? → 3-2 == 1 ✓ and 4-3 == 1 ✓ Valid
  • Since pattern c is valid, we check: dfs(5)

Step 3: dfs(5) - Processing index 5

  • Since 5 >= n (5 >= 5), we've successfully consumed all elements
  • Return True

Backtracking the results:

  • dfs(5) returns True
  • dfs(2) returns True (because pattern c was valid and dfs(5) returned True)
  • dfs(0) returns True (because pattern a was valid and dfs(2) returned True)

Final Result: True

The array [1,1,2,3,4] can be validly partitioned as:

  • [1,1] - satisfies condition 1 (two equal elements)
  • [2,3,4] - satisfies condition 3 (three consecutive increasing elements)

The memoization ensures that if we had encountered the same index multiple times through different paths, we would only compute it once and reuse the cached result.

Solution Implementation

1class Solution:
2    def validPartition(self, nums: List[int]) -> bool:
3        """
4        Determines if an array can be partitioned into valid subarrays.
5        Valid subarrays are:
6        - Exactly 2 equal elements
7        - Exactly 3 equal elements  
8        - Exactly 3 consecutive increasing elements (diff of 1)
9      
10        Args:
11            nums: List of integers to partition
12          
13        Returns:
14            True if valid partition exists, False otherwise
15        """
16        from functools import cache
17      
18        @cache
19        def can_partition_from_index(start_index: int) -> bool:
20            """
21            Recursively checks if array can be validly partitioned from given index.
22          
23            Args:
24                start_index: Current position in the array to partition from
25              
26            Returns:
27                True if valid partition exists from this index, False otherwise
28            """
29            # Base case: reached or passed the end of array
30            if start_index >= array_length:
31                return True
32          
33            # Check if we can form a 2-element subarray with equal values
34            has_two_equal = (start_index + 1 < array_length and 
35                            nums[start_index] == nums[start_index + 1])
36          
37            # Check if we can form a 3-element subarray with equal values
38            has_three_equal = (start_index + 2 < array_length and 
39                              nums[start_index] == nums[start_index + 1] == nums[start_index + 2])
40          
41            # Check if we can form a 3-element subarray with consecutive increasing values
42            has_three_consecutive = (start_index + 2 < array_length and 
43                                    nums[start_index + 1] - nums[start_index] == 1 and 
44                                    nums[start_index + 2] - nums[start_index + 1] == 1)
45          
46            # Try partitioning with 2-element subarray or 3-element subarray
47            return ((has_two_equal and can_partition_from_index(start_index + 2)) or 
48                   ((has_three_equal or has_three_consecutive) and can_partition_from_index(start_index + 3)))
49      
50        array_length = len(nums)
51        return can_partition_from_index(0)
52
1class Solution {
2    private int arrayLength;
3    private int[] inputArray;
4    private Boolean[] memoization;
5
6    /**
7     * Determines if the array can be partitioned into valid subarrays.
8     * Valid subarrays are:
9     * - Two equal elements
10     * - Three equal elements
11     * - Three consecutive increasing elements (difference of 1)
12     * 
13     * @param nums The input array to be partitioned
14     * @return true if valid partition exists, false otherwise
15     */
16    public boolean validPartition(int[] nums) {
17        arrayLength = nums.length;
18        this.inputArray = nums;
19        memoization = new Boolean[arrayLength];
20        return dfs(0);
21    }
22
23    /**
24     * Recursively checks if a valid partition exists starting from index i.
25     * Uses memoization to optimize repeated subproblems.
26     * 
27     * @param currentIndex The current starting index for partition
28     * @return true if valid partition exists from this index, false otherwise
29     */
30    private boolean dfs(int currentIndex) {
31        // Base case: successfully partitioned entire array
32        if (currentIndex >= arrayLength) {
33            return true;
34        }
35      
36        // Return memoized result if already computed
37        if (memoization[currentIndex] != null) {
38            return memoization[currentIndex];
39        }
40      
41        // Check condition 1: Two consecutive equal elements
42        boolean hasTwoEqualElements = currentIndex + 1 < arrayLength && 
43                                      inputArray[currentIndex] == inputArray[currentIndex + 1];
44      
45        // Check condition 2: Three consecutive equal elements
46        boolean hasThreeEqualElements = currentIndex + 2 < arrayLength && 
47                                        inputArray[currentIndex] == inputArray[currentIndex + 1] && 
48                                        inputArray[currentIndex + 1] == inputArray[currentIndex + 2];
49      
50        // Check condition 3: Three consecutive increasing elements (each differs by 1)
51        boolean hasThreeConsecutiveIncreasing = currentIndex + 2 < arrayLength && 
52                                                inputArray[currentIndex + 1] - inputArray[currentIndex] == 1 && 
53                                                inputArray[currentIndex + 2] - inputArray[currentIndex + 1] == 1;
54      
55        // Memoize and return result:
56        // - If two equal elements exist, try partitioning from index + 2
57        // - If three equal or three consecutive exist, try partitioning from index + 3
58        return memoization[currentIndex] = (hasTwoEqualElements && dfs(currentIndex + 2)) || 
59                                           ((hasThreeEqualElements || hasThreeConsecutiveIncreasing) && dfs(currentIndex + 3));
60    }
61}
62
1class Solution {
2public:
3    bool validPartition(vector<int>& nums) {
4        // Initialize the size of the array
5        arraySize = nums.size();
6        // Store the input array as a member variable
7        this->numsArray = nums;
8        // Initialize memoization array with -1 (unvisited state)
9        // -1: unvisited, 0: false, 1: true
10        memo.assign(arraySize, -1);
11        // Start DFS from index 0
12        return checkValidPartition(0);
13    }
14
15private:
16    int arraySize;
17    vector<int> memo;  // Memoization array to store results
18    vector<int> numsArray;  // Input array
19  
20    /**
21     * DFS function to check if a valid partition exists starting from index startIndex
22     * @param startIndex: The starting index to check partition from
23     * @return: true if valid partition exists, false otherwise
24     */
25    bool checkValidPartition(int startIndex) {
26        // Base case: if we've successfully partitioned the entire array
27        if (startIndex >= arraySize) {
28            return true;
29        }
30      
31        // Check memoized result
32        if (memo[startIndex] != -1) {
33            return memo[startIndex] == 1;
34        }
35      
36        // Check condition 1: Two equal elements
37        bool hasTwoEqual = (startIndex + 1 < arraySize) && 
38                           (numsArray[startIndex] == numsArray[startIndex + 1]);
39      
40        // Check condition 2: Three equal elements
41        bool hasThreeEqual = (startIndex + 2 < arraySize) && 
42                            (numsArray[startIndex] == numsArray[startIndex + 1]) && 
43                            (numsArray[startIndex + 1] == numsArray[startIndex + 2]);
44      
45        // Check condition 3: Three consecutive increasing elements (difference of 1)
46        bool hasThreeConsecutive = (startIndex + 2 < arraySize) && 
47                                   (numsArray[startIndex + 1] - numsArray[startIndex] == 1) && 
48                                   (numsArray[startIndex + 2] - numsArray[startIndex + 1] == 1);
49      
50        // Try partitioning with two elements (if valid) or three elements (if valid)
51        bool canPartition = (hasTwoEqual && checkValidPartition(startIndex + 2)) || 
52                           ((hasThreeEqual || hasThreeConsecutive) && checkValidPartition(startIndex + 3));
53      
54        // Store the result in memoization array
55        memo[startIndex] = canPartition ? 1 : 0;
56      
57        return memo[startIndex] == 1;
58    }
59};
60
1/**
2 * Determines if an array can be partitioned into valid subarrays.
3 * Valid subarrays are:
4 * - Two equal elements
5 * - Three equal elements
6 * - Three consecutive increasing elements (difference of 1)
7 * 
8 * @param nums - The input array of numbers
9 * @returns true if the array can be validly partitioned, false otherwise
10 */
11function validPartition(nums: number[]): boolean {
12    const arrayLength: number = nums.length;
13  
14    // Memoization array to store computed results
15    // -1: not computed, 0: false, 1: true
16    const memo: number[] = Array(arrayLength).fill(-1);
17  
18    /**
19     * Depth-first search to check if array can be partitioned from index i
20     * 
21     * @param currentIndex - The starting index for the current partition attempt
22     * @returns true if valid partition exists from currentIndex to end
23     */
24    const dfs = (currentIndex: number): boolean => {
25        // Base case: successfully partitioned entire array
26        if (currentIndex >= arrayLength) {
27            return true;
28        }
29      
30        // Return memoized result if already computed
31        if (memo[currentIndex] !== -1) {
32            return memo[currentIndex] === 1;
33        }
34      
35        // Check condition 1: Two consecutive equal elements
36        const hasTwoEqual: boolean = 
37            currentIndex + 1 < arrayLength && 
38            nums[currentIndex] === nums[currentIndex + 1];
39      
40        // Check condition 2: Three consecutive equal elements
41        const hasThreeEqual: boolean = 
42            currentIndex + 2 < arrayLength && 
43            nums[currentIndex] === nums[currentIndex + 1] && 
44            nums[currentIndex + 1] === nums[currentIndex + 2];
45      
46        // Check condition 3: Three consecutive increasing elements (difference of 1)
47        const hasThreeConsecutive: boolean = 
48            currentIndex + 2 < arrayLength && 
49            nums[currentIndex + 1] - nums[currentIndex] === 1 && 
50            nums[currentIndex + 2] - nums[currentIndex + 1] === 1;
51      
52        // Try partitioning with two elements or three elements based on conditions
53        const canPartition: boolean = 
54            (hasTwoEqual && dfs(currentIndex + 2)) || 
55            ((hasThreeEqual || hasThreeConsecutive) && dfs(currentIndex + 3));
56      
57        // Store result in memoization array
58        memo[currentIndex] = canPartition ? 1 : 0;
59      
60        return memo[currentIndex] === 1;
61    };
62  
63    // Start DFS from index 0
64    return dfs(0);
65}
66

Time and Space Complexity

Time Complexity: O(n)

The algorithm uses dynamic programming with memoization through the @cache decorator. Each index i from 0 to n-1 is visited at most once due to memoization. When we reach an index i, we perform constant time operations:

  • Checking boundary conditions (i + 1 < n, i + 2 < n)
  • Comparing array elements (nums[i] == nums[i + 1], etc.)
  • Computing differences (nums[i + 1] - nums[i])

After these O(1) checks, we make recursive calls to either dfs(i + 2) or dfs(i + 3). Since each state is cached, every index is processed exactly once, giving us a total of n states to compute. Therefore, the overall time complexity is O(n).

Space Complexity: O(n)

The space complexity consists of two components:

  1. Recursion Stack: In the worst case, the recursion depth can go up to O(n) when we always take 2-element partitions (calling dfs(i + 2) repeatedly), resulting in approximately n/2 recursive calls.
  2. Memoization Cache: The @cache decorator stores the result for each unique value of i. Since i ranges from 0 to n-1, we store at most n different states in the cache.

Both the recursion stack and the cache contribute O(n) space, so the total space complexity is O(n).

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

Common Pitfalls

1. Incorrect Condition Checking Order

A common mistake is checking conditions without proper boundary validation first, leading to index out of bounds errors.

Incorrect approach:

# Wrong - accesses nums[i+1] before checking if i+1 < n
has_two_equal = nums[i] == nums[i+1] and i + 1 < n

Correct approach:

# Right - checks boundary first with short-circuit evaluation
has_two_equal = i + 1 < n and nums[i] == nums[i+1]

2. Greedy Algorithm Temptation

Many developers initially try a greedy approach, always choosing the longest valid subarray at each position. This fails because sometimes taking a shorter valid subarray leads to the overall solution.

Example where greedy fails:

nums = [4,4,4,5,6,7]
# Greedy would take [4,4,4] first, leaving [5,6,7] which is valid
# But what if nums = [4,4,5,6,7,7]?
# Greedy takes [4,4] then [5,6,7], leaving single [7] - invalid!
# Correct partition: [4,4], [5,6,7], [7] - wait, that's still invalid
# Actually correct: Can't be partitioned, but point is greedy doesn't work

3. Missing Memoization

Without memoization, the recursive solution has exponential time complexity due to overlapping subproblems.

Without memoization (TLE for large inputs):

def dfs(i):
    if i >= n:
        return True
    # ... check conditions ...
    return result  # Same states computed multiple times

With memoization (efficient):

@cache
def dfs(i):
    # Same logic, but results are cached

4. Incorrect Consecutive Check

A subtle bug is checking consecutive elements incorrectly, such as only checking the difference between first and last elements.

Incorrect:

# Wrong - doesn't ensure ALL differences are 1
has_consecutive = i + 2 < n and nums[i+2] - nums[i] == 2

Correct:

# Right - checks each adjacent pair
has_consecutive = (i + 2 < n and 
                  nums[i+1] - nums[i] == 1 and 
                  nums[i+2] - nums[i+1] == 1)

5. Off-by-One Errors in Base Case

The base case should return True when i >= n, not just when i == n, because after taking the last valid subarray, the index might jump past the array length.

Incorrect:

if i == n:  # Misses case when last subarray is 3 elements
    return True

Correct:

if i >= n:  # Handles all cases properly
    return True
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

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

Load More