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:
-
Two equal elements: The subarray contains exactly 2 elements that are the same. For example,
[2,2]
is valid. -
Three equal elements: The subarray contains exactly 3 elements that are all the same. For example,
[4,4,4]
is valid. -
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 returnfalse
)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 returnfalse
)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 returntrue
)
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), returnTrue
as we've successfully partitioned all elements.
Recursive Cases:
At each position i
, we check three possible valid subarray patterns:
-
Two equal elements (
a
): Check ifi+1 < n
andnums[i] == nums[i+1]
. If true, we can form a 2-element subarray and recursively checkdfs(i+2)
. -
Three equal elements (
b
): Check ifi+2 < n
andnums[i] == nums[i+1] == nums[i+2]
. If true, we can form a 3-element subarray and recursively checkdfs(i+3)
. -
Three consecutive increasing elements (
c
): Check ifi+2 < n
,nums[i+1] - nums[i] == 1
, andnums[i+2] - nums[i+1] == 1
. If true, we can form a 3-element consecutive subarray and recursively checkdfs(i+3)
.
Decision Logic:
The function returns True
if:
- Pattern
a
is valid ANDdfs(i+2)
returnsTrue
, OR - Either pattern
b
orc
is valid ANDdfs(i+3)
returnsTrue
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 EvaluatorExample 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]
, lengthn = 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
andnums[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
andnums[4]-nums[3] == 1
? →3-2 == 1
✓ and4-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)
returnsTrue
dfs(2)
returnsTrue
(because pattern c was valid anddfs(5)
returnedTrue
)dfs(0)
returnsTrue
(because pattern a was valid anddfs(2)
returnedTrue
)
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:
- Recursion Stack: In the worst case, the recursion depth can go up to
O(n)
when we always take 2-element partitions (callingdfs(i + 2)
repeatedly), resulting in approximatelyn/2
recursive calls. - Memoization Cache: The
@cache
decorator stores the result for each unique value ofi
. Sincei
ranges from0
ton-1
, we store at mostn
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
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
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
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!