Check if There is a Valid Partition For The Array
MedPrompt
You are given a 0-indexed integer array nums. You have to partition the array into one or more contiguous subarrays.
We call a partition of the array valid if each of the obtained subarrays satisfies one of the following conditions:
- The subarray consists of exactly
2,equal elements. For example, the subarray[2,2]is good. - The subarray consists of exactly
3,equal elements. For example, the subarray[4,4,4]is good. - The subarray consists of exactly
3consecutive increasing elements, that is, the difference between adjacent elements is1. For example, the subarray[3,4,5]is good, but the subarray[1,3,5]is not.
Return true if the array has at least one valid partition. Otherwise, return false.
Example 1:
Input: nums = [4,4,4,5,6]
Output: true
Explanation: The array can be partitioned into the subarrays [4,4] and [4,5,6].
This partition is valid, so we return true.Example 2:
Input: nums = [1,1,1,2]
Output: false
Explanation: There is no valid partition for this array.
Constraints:
2 <= nums.length <= 1051 <= nums[i] <= 106
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses a straightforward recursive method to explore every possible way the array can be partitioned. It checks from the beginning of the array, trying to form valid subarrays of size 2 or 3, and then recursively checks the remainder of the array.
Algorithm
- Define a recursive function, say
solve(index), which returnstrueif the subarraynums[index...]can be validly partitioned, andfalseotherwise. - The base case for the recursion is when
indexreaches the end of the array (index == nums.length). This signifies that the entire array has been successfully partitioned, so we returntrue. - If
indexexceeds the array bounds, it's an invalid state, so we returnfalse. - In the recursive step, we explore all valid partitioning choices from the current
index:- Check for a 2-element partition: If
index + 1 < nandnums[index] == nums[index + 1], we can form a valid 2-element subarray. We then recursively callsolve(index + 2)to check if the rest of the array can be partitioned. If it can, we've found a valid partition for the whole array. - Check for a 3-element partition: If
index + 2 < n, we check ifnums[index...index+2]forms a valid 3-element subarray (either all equal or consecutive increasing). If it does, we recursively callsolve(index + 3). If the recursive call returnstrue, we've found a solution.
- Check for a 2-element partition: If
- If any of the recursive calls return
true, the function returnstrue. Otherwise, after trying all possibilities, it returnsfalse.
Walkthrough
The brute-force recursive approach directly translates the problem statement into a recursive structure. We define a function that attempts to partition the array starting from a given index. This function tries to match the first 2 or 3 elements against the valid partition rules. If a match is found, it recursively calls itself on the rest of the array. This process continues until the entire array is partitioned or all possibilities are exhausted.
The core idea is to check every path. For example, from index i, we can potentially move to i+2 or i+3. This branching leads to an exponential number of paths to explore.
Here is the Java implementation:
class Solution { public boolean validPartition(int[] nums) { return solve(0, nums); } private boolean solve(int index, int[] nums) { int n = nums.length; if (index == n) { return true; } // Condition 1: Subarray of size 2 if (index + 1 < n && nums[index] == nums[index + 1]) { if (solve(index + 2, nums)) { return true; } } // Condition 2 & 3: Subarray of size 3 if (index + 2 < n) { // 3 equal elements if (nums[index] == nums[index + 1] && nums[index + 1] == nums[index + 2]) { if (solve(index + 3, nums)) { return true; } } // 3 consecutive increasing elements if (nums[index] + 1 == nums[index + 1] && nums[index + 1] + 1 == nums[index + 2]) { if (solve(index + 3, nums)) { return true; } } } return false; }}Complexity
Time
O(2^n), where n is the length of the array. The number of recursive calls can grow exponentially, similar to a Fibonacci sequence, as `T(n) ≈ T(n-2) + T(n-3)`.
Space
O(n), where n is the length of the array. This is for the recursion stack depth in the worst case.
Trade-offs
Pros
Simple to conceptualize and implement.
Follows the problem definition closely.
Cons
Extremely inefficient due to a large number of redundant computations for the same subproblems.
Will likely result in a 'Time Limit Exceeded' error on platforms like LeetCode for the given constraints.
This approach improves upon the brute-force method by using dynamic programming to avoid recomputing results for overlapping subproblems. We use a DP array (or table) to store the results for subproblems, building the solution from the ground up (bottom-up).
Algorithm
- Create a boolean DP array,
dp, of sizen + 1, wheredp[i]will betrueif the prefixnums[0...i-1]can be validly partitioned. - Initialize
dp[0] = true. This is the base case, representing a valid partition for an empty prefix. - Iterate from
i = 2ton(since the smallest partition is of size 2). - Inside the loop, calculate
dp[i]based on the three rules:- Rule 1 (2 equal elements): If
nums[i-2] == nums[i-1], it means the last two elements form a valid partition. If the prefix before that,nums[0...i-3], was also validly partitionable (i.e.,dp[i-2]is true), thendp[i]can be set totrue. - Rule 2 & 3 (3-element subarray): If
i >= 3, check if the last three elementsnums[i-3...i-1]form a valid partition (either 3 equal or 3 consecutive increasing). If they do, and the prefixnums[0...i-4]was valid (dp[i-3]is true), thendp[i]can also be set totrue.
- Rule 1 (2 equal elements): If
- The final answer is the value of
dp[n].
Walkthrough
The key observation for optimizing the recursive solution is that we repeatedly solve the same subproblems. Dynamic programming is the perfect tool for this. We can solve this iteratively using a technique called tabulation.
We define dp[i] as a boolean value indicating whether the prefix of the array of length i (i.e., nums[0...i-1]) can be validly partitioned. Our goal is to find dp[n]. The state dp[i] depends on the results of smaller prefixes, specifically dp[i-2] and dp[i-3].
Here is the Java implementation:
class Solution { public boolean validPartition(int[] nums) { int n = nums.length; if (n < 2) { return false; } boolean[] dp = new boolean[n + 1]; dp[0] = true; for (int i = 2; i <= n; i++) { // Condition 1: Subarray of size 2 if (nums[i - 2] == nums[i - 1]) { dp[i] = dp[i] || dp[i - 2]; } // Condition 2 & 3: Subarray of size 3 if (i >= 3) { boolean threeEqual = nums[i - 3] == nums[i - 2] && nums[i - 2] == nums[i - 1]; boolean threeConsecutive = nums[i - 3] + 1 == nums[i - 2] && nums[i - 2] + 1 == nums[i - 1]; if (threeEqual || threeConsecutive) { dp[i] = dp[i] || dp[i - 3]; } } } return dp[n]; }}A top-down DP approach with memoization would also achieve the same time and space complexity.
Complexity
Time
O(n), as we iterate through the array once to fill the DP table.
Space
O(n), for the DP array `dp` of size `n+1`.
Trade-offs
Pros
Efficient with a linear time complexity.
Guaranteed to pass within the time limits.
Avoids deep recursion and potential stack overflow issues that can arise in the pure recursive approach.
Cons
Requires O(n) extra space for the DP array, which might be a concern for very large inputs, although it's acceptable for the given constraints.
This is the most efficient approach, optimizing the space complexity of the standard DP solution. By observing the dependencies in the DP recurrence relation, we can see that we only need to store the last three results to compute the next one. This allows us to reduce the space from O(n) to O(1).
Algorithm
- The calculation of
dp[i]only depends ondp[i-2]anddp[i-3]. This allows us to optimize the space. - Instead of a full DP array, we only need to maintain the last three DP states. Let's use three boolean variables:
three_back,two_back, andone_backto representdp[i-3],dp[i-2], anddp[i-1]respectively. - Initialize the variables based on the base cases:
three_back = true(representingdp[0])two_back = false(representingdp[1], which is not reachable by a valid partition)one_back = (nums[0] == nums[1])(representingdp[2])
- Handle the
n=2case separately. - Iterate from
i = 3ton. In each iteration, calculatecurrent_dpbased ontwo_backandthree_back.current_dpis true if(nums[i-2] == nums[i-1] && two_back)is true, or if(valid 3-element partition ending at i-1 && three_back)is true.
- After computing
current_dp, update the state variables for the next iteration:three_backbecomestwo_back,two_backbecomesone_back, andone_backbecomescurrent_dp. - The final result is the value of
one_backafter the loop finishes.
Walkthrough
We can further optimize the bottom-up DP approach in terms of space. The value of dp[i] depends only on dp[i-2] and dp[i-3]. This means that at any point in our iteration, we only need to know the results for the last three indices. We can achieve this by using a few variables to store these previous states, effectively reducing the space complexity to constant.
We'll use three variables to keep track of the necessary previous DP values as we iterate through the array. This avoids allocating an array of size n+1 and is the most optimal solution for this problem.
Here is the space-optimized Java implementation:
class Solution { public boolean validPartition(int[] nums) { int n = nums.length; // We only need to store the last 3 dp values. // Let three_back, two_back, one_back be dp[i-3], dp[i-2], dp[i-1] boolean three_back = true; // Base case for dp[0] boolean two_back = false; // dp[1] is always false boolean one_back = (n >= 2 && nums[0] == nums[1]); // Base case for dp[2] if (n == 2) { return one_back; } for (int i = 3; i <= n; i++) { boolean current_dp = false; // Case 1: Last two elements form a valid partition (e.g., [x,x]) if (nums[i - 2] == nums[i - 1]) { current_dp = current_dp || two_back; // check dp[i-2] } // Case 2: Last three elements form a valid partition boolean threeEqual = nums[i - 3] == nums[i - 2] && nums[i - 2] == nums[i - 1]; boolean threeConsecutive = nums[i - 3] + 1 == nums[i - 2] && nums[i - 2] + 1 == nums[i - 1]; if (threeEqual || threeConsecutive) { current_dp = current_dp || three_back; // check dp[i-3] } // Update states for the next iteration three_back = two_back; two_back = one_back; one_back = current_dp; } return one_back; // This holds the final result, dp[n] }}Complexity
Time
O(n), as we still need to iterate through the array once.
Space
O(1), as we only use a few variables to store the previous DP states, regardless of the input size.
Trade-offs
Pros
Most optimal solution with linear time and constant space complexity.
Highly efficient for very large inputs.
Cons
The logic can be slightly more complex to reason about due to the rolling variables instead of a direct-access array.
Solutions
Solution
class Solution {private int n;private int[] f;private int[] nums;public boolean validPartition(int[] nums) { this.nums = nums; n = nums.length; f = new int[n]; Arrays.fill(f, -1); return dfs(0); }private boolean dfs(int i) { if (i == n) { return true; } if (f[i] != -1) { return f[i] == 1; } boolean res = false; if (i < n - 1 && nums[i] == nums[i + 1]) { res = res || dfs(i + 2); } if (i < n - 2 && nums[i] == nums[i + 1] && nums[i + 1] == nums[i + 2]) { res = res || dfs(i + 3); } if (i < n - 2 && nums[i + 1] - nums[i] == 1 && nums[i + 2] - nums[i + 1] == 1) { res = res || dfs(i + 3); } f[i] = res ? 1 : 0; return res; }}class Solution {public: vector<int> f; vector<int> nums; int n; bool validPartition(vector<int> &nums) { n = nums.size(); this->nums = nums; f.assign(n, -1); return dfs(0); } bool dfs(int i) { if (i == n) return true; if (f[i] != -1) return f[i] == 1; bool res = false; if (i < n - 1 && nums[i] == nums[i + 1]) res = res || dfs(i + 2); if (i < n - 2 && nums[i] == nums[i + 1] && nums[i + 1] == nums[i + 2]) res = res || dfs(i + 3); if (i < n - 2 && nums[i + 1] - nums[i] == 1 && nums[i + 2] - nums[i + 1] == 1) res = res || dfs(i + 3); f[i] = res ? 1 : 0; return res; }};class Solution: def validPartition(self, nums: List[int]) -> bool: @ cache def dfs(i): if i == n: return True res = False if i < n - 1 and nums[i] == nums[i + 1]: res = res or dfs(i + 2) if i < n - 2 and nums[i] == nums[i + 1] and nums[i + 1] == nums[i + 2]: res = res or dfs(i + 3) if (i < n - 2 and nums[i + 1] - nums[i] == 1 and nums[i + 2] - nums[i + 1] == 1): res = res or dfs(i + 3) return res n = len(nums) return dfs(0)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.