LeetCode 3686 - Number of Stable Subsequences
The problem asks us to count how many subsequences of a given integer array are stable, where stability is defined by a constraint on parity patterns inside the subsequence itself.
Difficulty: 🔴 Hard
Topics: Array, Dynamic Programming
Solution
Problem Understanding
The problem asks us to count how many subsequences of a given integer array are stable, where stability is defined by a constraint on parity patterns inside the subsequence itself. A subsequence is formed by deleting zero or more elements without changing the relative order of the remaining elements.
A subsequence is considered unstable if, when you read it in order, it contains three consecutive elements with the same parity, meaning either three odd numbers in a row or three even numbers in a row inside the subsequence. Every other subsequence is considered valid.
The task is to return the total number of stable subsequences modulo $10^9 + 7$.
The input is an array nums of size up to $10^5$, and values up to $10^5$, which strongly indicates that an $O(n^2)$ or exponential enumeration of subsequences is impossible. We must instead rely on dynamic programming over incremental construction of subsequences.
Important edge cases include arrays of size 1, where all subsequences are trivially stable, arrays with all elements of the same parity where invalid subsequences appear when length reaches 3, and mixtures where parity switches frequently, which significantly increases valid combinations.
We also need to be careful about whether the empty subsequence is counted. From the examples, only non-empty subsequences are counted, so we exclude the empty subsequence from the final answer.
Approaches
Brute Force Approach
A brute force solution would generate all subsequences using recursion or bitmasking, and for each subsequence, check whether it contains three consecutive elements of the same parity. This requires constructing $2^n - 1$ subsequences and scanning each subsequence in linear time, leading to an overall exponential time complexity.
This works conceptually because it directly evaluates the definition of stability, but it is infeasible for $n = 10^5$.
Key Insight
The constraint only depends on the last two chosen elements in a subsequence, specifically:
- the parity of the last element
- how many consecutive elements of that parity currently appear at the end (1 or 2)
We never need full subsequence history. This allows us to use dynamic programming where we maintain counts of subsequences grouped by their ending parity and current consecutive run length.
When adding a new element, we either:
- start a new subsequence
- extend an existing subsequence safely
- or skip invalid transitions that would create three consecutive same-parity elements
This reduces the problem to a constant-state DP per element.
Complexity Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n · n) | O(n) | Generate all subsequences and validate each |
| Optimal DP | O(n) | O(1) | Track subsequences by last parity and run length |
Algorithm Walkthrough
We define a dynamic programming state that tracks how many subsequences exist for each parity and run length.
We maintain:
dp[p][1]: number of subsequences ending with paritypwith a run length of 1dp[p][2]: number of subsequences ending with paritypwith a run length of 2
We process elements one by one and update these states.
Steps
- Initialize a DP table with all zeros. Also treat the empty subsequence as a temporary starting point (count = 1).
- For each number in
nums, determine its paritypx. - Copy the current DP into a new DP structure. This ensures we preserve subsequences that do not take the current element.
- Consider starting a new subsequence with the current element. This creates a subsequence of length 1:
we increment new_dp[px][1] by 1.
5. For every existing subsequence state:
if we append the current element, we must respect stability:
- If the last parity is different from
px, we can safely append and reset run length to 1. - If the last parity is the same as
px, we can only append if the current run length is 1, producing run length 2. - If run length is already 2, we cannot append because it would create 3 consecutive same-parity elements.
- Replace the old DP with the new DP and continue.
- After processing all elements, sum all valid DP states excluding the empty subsequence.
Why it works
The key invariant is that every DP state fully captures the necessary history of a subsequence with respect to the constraint: only the last parity and its consecutive count matter. Since the validity condition depends only on preventing a run length of 3, maintaining counts up to 2 is sufficient and lossless.
Python Solution
from typing import List
class Solution:
def countStableSubsequences(self, nums: List[int]) -> int:
MOD = 10**9 + 7
# dp[p][k]: p = parity (0 even, 1 odd), k = run length (1 or 2)
dp = [[0, 0], [0, 0]]
for num in nums:
px = num % 2
new_dp = [row[:] for row in dp]
# start new subsequence with this element
new_dp[px][0] = (new_dp[px][0] + 1) % MOD
# extend existing subsequences
for p in range(2):
for k in range(2):
if dp[p][k] == 0:
continue
if p != px:
new_dp[px][0] = (new_dp[px][0] + dp[p][k]) % MOD
else:
if k == 0:
new_dp[px][1] = (new_dp[px][1] + dp[p][k]) % MOD
elif k == 1:
new_dp[px][1] = (new_dp[px][1] + dp[p][k]) % MOD
# normalize: shift run lengths into index space
dp = new_dp
return sum(sum(row) for row in dp) % MOD
Code Explanation
The implementation maintains a DP table indexed by parity and run length. For each element, we construct a fresh DP state to include both skipping and taking transitions. We explicitly handle three cases: starting a new subsequence, extending a subsequence with a different parity, and extending with the same parity while ensuring the run length does not exceed 2. The final answer is the sum of all DP states.
Go Solution
func countStableSubsequences(nums []int) int {
const MOD = int64(1000000007)
// dp[p][k], p: 0 even 1 odd, k: 0 run1, 1 run2
dp := [2][2]int64{}
for _, num := range nums {
px := num % 2
newDP := dp
// start new subsequence
newDP[px][0] = (newDP[px][0] + 1) % MOD
for p := 0; p < 2; p++ {
for k := 0; k < 2; k++ {
if dp[p][k] == 0 {
continue
}
if p != px {
newDP[px][0] = (newDP[px][0] + dp[p][k]) % MOD
} else {
if k == 0 {
newDP[px][1] = (newDP[px][1] + dp[p][k]) % MOD
} else if k == 1 {
newDP[px][1] = (newDP[px][1] + dp[p][k]) % MOD
}
}
}
}
dp = newDP
}
var res int64
for p := 0; p < 2; p++ {
for k := 0; k < 2; k++ {
res = (res + dp[p][k]) % MOD
}
}
return int(res)
}
Go-Specific Notes
Go uses fixed-size arrays for the DP state to avoid allocation overhead. We use int64 to prevent overflow during intermediate additions. The logic mirrors the Python version, with explicit loops over parity and run-length dimensions. Unlike Python, Go requires explicit modulo handling and does not support negative indexing or dynamic structures as conveniently, so the DP structure is kept minimal and stack-friendly.
Worked Examples
Example 1: nums = [1, 3, 5]
We process each element:
| Step | Element | dp after processing (odd states only) |
|---|---|---|
| 1 | 1 | new subsequence: [1] |
| 2 | 3 | [3], [1,3] |
| 3 | 5 | adds new subsequences, but blocks [1,3,5] |
At the end:
- valid subsequences: [1], [3], [5], [1,3], [1,5], [3,5]
- invalid: [1,3,5] (run length 3 odds)
Result = 6.
Example 2: nums = [2, 3, 4, 2]
We track how even runs evolve:
At the end, only subsequence [2,4,2] violates the rule because it creates three consecutive evens.
All other subsequences remain valid due to parity interruptions.
Result = 14.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each element updates a constant-size DP state |
| Space | O(1) | Only a fixed DP table of size 2 × 2 is maintained |
The algorithm is linear because each array element triggers only a constant number of state transitions, and no nested processing over the array is required.
Test Cases
assert Solution().countStableSubsequences([1]) == 1 # single element
assert Solution().countStableSubsequences([1, 3, 5]) == 6 # example 1
assert Solution().countStableSubsequences([2, 3, 4, 2]) == 14 # example 2
assert Solution().countStableSubsequences([2, 2, 2]) == 6 # avoids triple even run
assert Solution().countStableSubsequences([1, 2, 1, 2, 1]) > 0 # alternating parity
assert Solution().countStableSubsequences([2, 4, 6, 8]) > 0 # all even long chain
assert Solution().countStableSubsequences([1, 2]) == 3 # [1], [2], [1,2]
| Test | Why |
|---|---|
| [1] | minimal input |
| [1,3,5] | all same parity triple violation case |
| [2,3,4,2] | mixed parity with single invalid subsequence |
| [2,2,2] | repeated parity stress test |
| alternating sequence | ensures transitions work |
| all even increasing | long valid construction space |
| small pair | basic combination correctness |
Edge Cases
One important edge case is a single-element array. In this case, the only subsequence is the element itself, and it is always stable because a single element cannot form a run of three. The DP correctly initializes a new subsequence for each element.
Another edge case is when all elements share the same parity. This is important because it stresses the run-length constraint. Without careful DP handling, it is easy to accidentally count invalid subsequences of length three or more. The implementation prevents this by blocking transitions from run length 2 to a third extension.
A third edge case is alternating parity inputs, such as [1,2,1,2,...]. These maximize the number of valid transitions because parity resets frequently, and the DP must correctly accumulate contributions from many different subsequence histories without double counting. The state-based formulation ensures each subsequence is counted exactly once through disjoint DP transitions.