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.

LeetCode Problem 1955

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:

  1. count0 - the number of subsequences consisting only of 0s.
  2. count1 - the number of subsequences of the form 0+1+.
  3. count2 - the number of subsequences of the form 0+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

  1. Initialize three counters count0, count1, and count2 to 0. These represent subsequences ending with 0, subsequences ending with 1, and subsequences ending with 2, respectively.
  2. Iterate through each number in the input array nums.
  3. If the current number is 0, it can start a new subsequence or extend all existing subsequences of 0s. Therefore, update count0 as count0 = 2 * count0 + 1. The multiplication by 2 accounts for choosing or not choosing this 0 in all existing subsequences.
  4. If the current number is 1, it can extend existing 0 subsequences into 0+1 subsequences, or extend existing 0+1 subsequences. Therefore, update count1 as count1 = 2 * count1 + count0.
  5. If the current number is 2, it can extend existing 0+1 subsequences into 0+1+2 subsequences, or extend existing 0+1+2 subsequences. Therefore, update count2 as count2 = 2 * count2 + count1.
  6. Take the modulo 10^9 + 7 after each update to prevent integer overflow.
  7. After processing all numbers, count2 contains 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

|