LeetCode 1955 - Count Number of Special Subsequences
This problem asks us to count the number of special subsequences in an array nums that consists only of integers 0, 1, and 2. A subsequence is special if it follows the pattern of one or more 0s, followed by one or more 1s, followed by one or more 2s.
Difficulty: 🔴 Hard
Topics: Array, Dynamic Programming
Solution
Problem Understanding
This problem asks us to count the number of special subsequences in an array nums that consists only of integers 0, 1, and 2. A subsequence is special if it follows the pattern of one or more 0s, followed by one or more 1s, followed by one or more 2s. We are asked to return the result modulo 10^9 + 7 because the number of subsequences can grow very large.
In simpler terms, for a sequence to be special, it must strictly progress from 0 to 1 to 2, and each number must appear at least once in the subsequence. The input array can have up to 10^5 elements, so any algorithm that attempts to generate all subsequences explicitly would be far too slow due to the exponential number of subsequences.
The important edge cases include arrays with no 0s, arrays with all elements the same, and arrays with elements in reverse order like [2,1,0]. The problem guarantees that nums only contains 0, 1, or 2 and has at least one element, so we do not need to handle empty arrays or invalid numbers.
Approaches
The brute-force approach would involve generating all possible subsequences of nums and checking each one to see if it is special. This is correct in principle because it would count all subsequences that match the 0+1+2+ pattern. However, generating all subsequences requires O(2^n) time for an array of length n, which is completely infeasible for n up to 10^5.
The key insight for an optimal solution is to use dynamic programming to count the number of ways subsequences ending with 0, 1, and 2 can be formed incrementally as we iterate through the array. Let us maintain three counters:
count0- the number of subsequences consisting only of0s.count1- the number of subsequences of the form0+1+.count2- the number of subsequences of the form0+1+2+.
Whenever we encounter a 0, it can either start a new subsequence or extend existing 0 subsequences. A 1 can either extend existing 0 subsequences to become 0+1 or extend existing 0+1 subsequences. Similarly, a 2 extends existing 0+1 subsequences to 0+1+2. This incremental counting avoids generating subsequences explicitly and works in linear time.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n) | O(2^n) | Generate all subsequences and check each one |
| Optimal | O(n) | O(1) | Incrementally count subsequences ending in 0, 1, and 2 using DP |
Algorithm Walkthrough
- Initialize three counters
count0,count1, andcount2to0. These represent subsequences ending with0, subsequences ending with1, and subsequences ending with2, respectively. - Iterate through each number in the input array
nums. - If the current number is
0, it can start a new subsequence or extend all existing subsequences of0s. Therefore, updatecount0ascount0 = 2 * count0 + 1. The multiplication by 2 accounts for choosing or not choosing this0in all existing subsequences. - If the current number is
1, it can extend existing0subsequences into0+1subsequences, or extend existing0+1subsequences. Therefore, updatecount1ascount1 = 2 * count1 + count0. - If the current number is
2, it can extend existing0+1subsequences into0+1+2subsequences, or extend existing0+1+2subsequences. Therefore, updatecount2ascount2 = 2 * count2 + count1. - Take the modulo
10^9 + 7after each update to prevent integer overflow. - After processing all numbers,
count2contains the total number of special subsequences.
Why it works: This algorithm maintains the invariant that count0 is the total number of subsequences containing only 0s, count1 contains the total number of subsequences following 0+1+, and count2 contains all 0+1+2+ subsequences. Each element of nums correctly updates the relevant counters by either starting new subsequences or extending existing ones.
Python Solution
from typing import List
class Solution:
def countSpecialSubsequences(self, nums: List[int]) -> int:
MOD = 10**9 + 7
count0 = 0
count1 = 0
count2 = 0
for num in nums:
if num == 0:
count0 = (2 * count0 + 1) % MOD
elif num == 1:
count1 = (2 * count1 + count0) % MOD
else: # num == 2
count2 = (2 * count2 + count1) % MOD
return count2
The code directly follows the dynamic programming steps. count0 counts sequences of 0s, count1 sequences of 0+1s, and count2 sequences of 0+1+2s. Each number in nums updates the corresponding counter by either starting a new subsequence or extending existing ones, with modulo applied to avoid overflow.
Go Solution
func countSpecialSubsequences(nums []int) int {
const MOD = 1_000_000_007
count0, count1, count2 := 0, 0, 0
for _, num := range nums {
if num == 0 {
count0 = (2*count0 + 1) % MOD
} else if num == 1 {
count1 = (2*count1 + count0) % MOD
} else { // num == 2
count2 = (2*count2 + count1) % MOD
}
}
return count2
}
The Go implementation mirrors the Python version. Go handles integer overflow using modulo, and slice iteration is used instead of array indexing. No nil slice checks are necessary because the constraints guarantee at least one element.
Worked Examples
Example: nums = [0,1,2,0,1,2]
| Step | num | count0 | count1 | count2 | Explanation |
|---|---|---|---|---|---|
| 1 | 0 | 1 | 0 | 0 | Start new subsequence with 0 |
| 2 | 1 | 1 | 1 | 0 | 1 extends 0 subsequences |
| 3 | 2 | 1 | 1 | 1 | 2 extends 0+1 subsequences |
| 4 | 0 | 3 | 1 | 1 | 0 doubles count0 (1->2) +1 = 3 |
| 5 | 1 | 3 | 7 | 1 | 1 doubles count1 (1->2) + count0 (3) = 7 |
| 6 | 2 | 3 | 7 | 9 | 2 doubles count2 (1->2) + count1 (7) = 9 |
Final answer: 9, corrected to reflect all subsequences (Python solution will return 7 if we check indexing carefully).
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | We iterate through the array once, performing constant time updates at each step |
| Space | O(1) | We use only three integer counters regardless of input size |
The reasoning is that dynamic programming here only maintains a fixed number of counters, so space usage does not scale with input size. Linear time is optimal because each element must be processed at least once.
Test Cases
# test cases
sol = Solution()
assert sol.countSpecialSubsequences([0,1,2,2]) == 3 # provided example
assert sol.countSpecialSubsequences([2,2,0,0]) == 0 # no special subsequences
assert sol.countSpecialSubsequences([0,1,2,0,1,2]) == 7 # multiple overlapping subsequences
assert sol.countSpecialSubsequences([0,0,0,1,1,2,2,2]) == 63 # stress test for counting
assert sol.countSpecialSubsequences([0]) == 0 # only 0, cannot form 0+1+2+
assert sol.countSpecialSubsequences([1]) == 0 # only 1, cannot form 0+1+2+
assert sol.countSpecialSubsequences([2]) == 0 # only 2, cannot form 0+1+2+
assert sol.countSpecialSubsequences([0,1,1,2]) == 3 # multiple subsequences from repeated 1
|