title: Check if There is a Valid Partition For The Array slug: check-if-there-is-a-valid-partition-for-the-array difficulty: medium leetcode_id: 2369 leetcode_url: https://leetcode.com/problems/check-if-there-is-a-valid-partition-for-the-array/ categories: - arrays - dynamic-programming patterns: - slug: dynamic-programming is_optimal: true function_signature: "def valid_partition(nums: list[int]) -> bool:" test_cases: visible: - input: { nums: [4, 4, 4, 5, 6] } expected: true - input: { nums: [1, 1, 1, 2] } expected: false hidden: - input: { nums: [1, 1] } expected: true - input: { nums: [1, 1, 1] } expected: true - input: { nums: [1, 2, 3] } expected: true - input: { nums: [1, 2] } expected: false - input: { nums: [5, 5, 5, 5] } expected: true - input: { nums: [1, 2, 3, 3, 3] } expected: true - input: { nums: [1, 1, 2, 2] } expected: true description: | 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: 1. The subarray consists of **exactly** `2` equal elements. For example, the subarray `[2,2]` is good. 2. The subarray consists of **exactly** `3` equal elements. For example, the subarray `[4,4,4]` is good. 3. The subarray consists of **exactly** `3` consecutive increasing elements, that is, the difference between adjacent elements is `1`. 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`. constraints: | - `2 <= nums.length <= 10^5` - `1 <= nums[i] <= 10^6` examples: - 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." - input: "nums = [1,1,1,2]" output: "false" explanation: "There is no valid partition for this array." explanation: intuition: | Imagine you're walking along the array from left to right, deciding at each position: "Can I end a valid subarray here?" The key insight is that this is a **decision problem with overlapping subproblems**. Whether you can validly partition the array up to position `i` depends on whether you could validly partition up to positions `i-2` or `i-3` — and whether the last 2 or 3 elements form a valid group. Think of it like building with blocks: you can only place a new block (2-element or 3-element subarray) if the foundation beneath it is solid (previous positions can be validly partitioned). This recursive structure with overlapping subproblems is the hallmark of dynamic programming. The three valid "blocks" you can place are: - Two equal elements `[a, a]` - Three equal elements `[a, a, a]` - Three consecutive elements `[a, a+1, a+2]` At each position, you check if any of these blocks can end here, and if so, whether the rest of the array before that block is also valid. approach: | We solve this using **Dynamic Programming** where `dp[i]` represents whether the subarray `nums[0..i-1]` (the first `i` elements) can be validly partitioned. **Step 1: Initialise the DP array** - `dp[0]`: Set to `True` — an empty prefix is trivially valid (base case) - `dp[1..n]`: Set to `False` initially — we'll determine validity through transitions   **Step 2: Define helper functions for valid subarrays** - `two_equal(i)`: Check if `nums[i-1] == nums[i-2]` (last two elements are equal) - `three_equal(i)`: Check if `nums[i-1] == nums[i-2] == nums[i-3]` (last three elements are equal) - `three_consecutive(i)`: Check if `nums[i-3] + 1 == nums[i-2]` and `nums[i-2] + 1 == nums[i-1]` (last three form a consecutive sequence)   **Step 3: Fill the DP array** - For each position `i` from `2` to `n`: - If `i >= 2` and `two_equal(i)` and `dp[i-2]` is `True`, set `dp[i] = True` - If `i >= 3` and `three_equal(i)` and `dp[i-3]` is `True`, set `dp[i] = True` - If `i >= 3` and `three_consecutive(i)` and `dp[i-3]` is `True`, set `dp[i] = True`   **Step 4: Return the result** - Return `dp[n]` — whether the entire array can be validly partitioned   This approach works because we systematically check all possible ways to end a valid subarray at each position, building on previously computed valid partitions. common_pitfalls: - title: Greedy Doesn't Work description: | A tempting approach is to greedily pick the first valid subarray you find and continue. For example, with `[4,4,4,5,6]`, you might greedily take `[4,4,4]` leaving `[5,6]` which is invalid. But the correct partition is `[4,4]` + `[4,5,6]`. Greedy fails because the optimal choice at one position depends on future elements. Dynamic programming explores all possibilities. wrong_approach: "Greedily pick first valid subarray found" correct_approach: "Use DP to consider all valid partition combinations" - title: Off-by-One Index Errors description: | The DP recurrence involves looking back 2 or 3 positions. It's easy to make index errors when checking `nums[i-1]`, `nums[i-2]`, `nums[i-3]`. Be careful: if `dp[i]` represents the first `i` elements, then `nums[i-1]` is the last element in that prefix. When checking two equal elements, you compare `nums[i-1]` and `nums[i-2]`, not `nums[i]` and `nums[i-1]`. wrong_approach: "Inconsistent indexing between dp array and nums array" correct_approach: "Clearly define what dp[i] represents and derive indices consistently" - title: Forgetting the Base Case description: | Without `dp[0] = True`, no partition can ever be valid. The DP transitions require that `dp[i-2]` or `dp[i-3]` be `True` for `dp[i]` to become `True`. If `dp[0]` is `False`, the chain breaks. `dp[0] = True` means "partitioning zero elements is trivially valid" — it's the foundation that allows the first 2 or 3 element group to be placed. key_takeaways: - "**DP for partition problems**: When deciding how to split an array, define `dp[i]` as whether the prefix of length `i` can be validly partitioned" - "**Multiple transitions**: At each position, check all valid ways to end a subarray and combine with previous DP states" - "**Greedy fails for partitioning**: The optimal local choice doesn't guarantee a global solution when future elements matter" - "**Space optimisation possible**: Since we only look back 3 positions, we can reduce space from O(n) to O(1) using rolling variables" time_complexity: "O(n). We iterate through the array once, performing O(1) checks at each position." space_complexity: "O(n) for the DP array. Can be optimised to O(1) by only keeping the last 3 DP values." solutions: - approach_name: Dynamic Programming is_optimal: true code: | def valid_partition(nums: list[int]) -> bool: n = len(nums) # dp[i] = True if nums[0..i-1] can be validly partitioned dp = [False] * (n + 1) # Base case: empty prefix is valid dp[0] = True for i in range(2, n + 1): # Check if last 2 elements are equal if nums[i - 1] == nums[i - 2] and dp[i - 2]: dp[i] = True # Check if last 3 elements are equal if i >= 3 and nums[i - 1] == nums[i - 2] == nums[i - 3] and dp[i - 3]: dp[i] = True # Check if last 3 elements are consecutive increasing if i >= 3: if nums[i - 3] + 1 == nums[i - 2] and nums[i - 2] + 1 == nums[i - 1]: if dp[i - 3]: dp[i] = True return dp[n] explanation: | **Time Complexity:** O(n) — Single pass through the array with O(1) work at each step. **Space Complexity:** O(n) — DP array of size n+1. We build up the solution by checking at each position whether we can end a valid 2-element or 3-element subarray, combining with previously computed valid partitions. - approach_name: Space-Optimised DP is_optimal: true code: | def valid_partition(nums: list[int]) -> bool: n = len(nums) # Only need last 3 dp values: dp[i-3], dp[i-2], dp[i-1] # Represent as dp0, dp1, dp2 where dp2 is most recent dp0, dp1, dp2 = True, False, False for i in range(2, n + 1): current = False # Check if last 2 elements are equal if nums[i - 1] == nums[i - 2] and dp1: current = True # Check if last 3 elements are equal if i >= 3 and nums[i - 1] == nums[i - 2] == nums[i - 3] and dp0: current = True # Check if last 3 elements are consecutive if i >= 3: if nums[i - 3] + 1 == nums[i - 2] and nums[i - 2] + 1 == nums[i - 1]: if dp0: current = True # Shift the window dp0, dp1, dp2 = dp1, dp2, current return dp2 explanation: | **Time Complexity:** O(n) — Same as standard DP approach. **Space Complexity:** O(1) — Only three variables instead of an array. Since we only ever look back at most 3 positions in our DP array, we can use a sliding window of 3 variables instead of storing the entire array. This reduces memory usage from O(n) to O(1). - approach_name: Recursive with Memoisation is_optimal: false code: | def valid_partition(nums: list[int]) -> bool: n = len(nums) memo = {} def can_partition(start: int) -> bool: # Base case: reached the end if start == n: return True if start in memo: return memo[start] result = False # Try 2 equal elements if start + 2 <= n and nums[start] == nums[start + 1]: result = result or can_partition(start + 2) # Try 3 equal elements if start + 3 <= n and nums[start] == nums[start + 1] == nums[start + 2]: result = result or can_partition(start + 3) # Try 3 consecutive elements if start + 3 <= n: if nums[start] + 1 == nums[start + 1] and nums[start + 1] + 1 == nums[start + 2]: result = result or can_partition(start + 3) memo[start] = result return result return can_partition(0) explanation: | **Time Complexity:** O(n) — Each starting position is computed once due to memoisation. **Space Complexity:** O(n) — Memoisation dictionary and recursion stack. This top-down approach recursively tries all valid partitions from each starting position. Memoisation prevents redundant computation. While equivalent in complexity, the iterative DP approach is generally preferred for avoiding recursion stack overhead.