Distinct Subsequences Count: Optimization and Implementation

Dynamic Programming By Michael Chen, Distinguished Engineer 20 min read

1. Introduction and Core Concepts

Distinct subsequences count is a classic dynamic programming problem that asks a deceptively simple question: given two strings S and T, how many distinct ways can T be obtained as a subsequence of S? Unlike substrings, which require contiguous characters, a subsequence is formed by deleting zero or more characters from the original string without changing the order of the remaining characters. For example, "ace" is a subsequence of "abcde", while "aec" is not.

This problem is formally known as LeetCode 115 - Distinct Subsequences and appears frequently in advanced algorithm interviews, competitive programming, and string processing pipelines. The distinct subsequences count matters because it generalizes pattern matching from a boolean question ("does T appear in S?") to a quantitative one ("how many times does T appear as a pattern in S?"). This quantitative view is essential when we need to rank matches, estimate probabilities, or count combinatorial possibilities.

There are two closely related variants that are often confused. The first variant, which we focus on in this article, counts the number of times a target string T appears as a subsequence of a source string S. The second variant counts the total number of distinct subsequences (including the empty string) of a single string S itself, where duplicates due to repeated characters must be handled carefully. Both variants share the same DP intuition but have different recurrences and optimizations. Understanding the first variant provides a foundation for solving the second.

Why does distinct subsequences count matter beyond interviews? At its core, it models subsequence embedding, a fundamental operation in bioinformatics, natural language processing, and data compression. When you search for a DNA motif inside a genome, count how many ways a typo-tolerant query can be matched in a database, or estimate the redundancy of subsequences in a compressed file, you are implicitly solving a distinct subsequences counting problem. The efficiency of the solution directly impacts the scalability of these systems.

Core Definitions

A subsequence of a string S of length n is any string that can be derived by deleting k characters (where 0 <= k <= n) while preserving order. There are 2^n total subsequences if we count each index selection separately, but many may be identical as strings if S contains repeated characters. When we count distinct subsequences of T in S, we count index selections, not distinct strings. For example, if S = "rabbbit" and T = "rabbit", there are 3 ways to match, corresponding to three different choices of which 'b' characters to use.

The problem exhibits optimal substructure and overlapping subproblems, the two hallmarks of dynamic programming. The decision at each character of S — to use it to match the current character of T or to skip it — leads to a recursive decomposition where the same subproblems (i, j) representing prefixes S[0:i] and T[0:j] are solved repeatedly.

2. How It Works

The mechanism behind distinct subsequences count is best understood through a prefix-based recurrence. Let dp[i][j] denote the number of distinct subsequences of T[0:j] in S[0:i], where i and j are prefix lengths. We want dp[n][m] where n = len(S) and m = len(T). The base cases are intuitive: there is exactly one way to form an empty target T = "" from any source — by deleting all characters — so dp[i][0] = 1 for all i. Conversely, there are zero ways to form a non-empty T from an empty S, so dp[0][j] = 0 for j > 0.

For the general transition, consider the last characters S[i-1] and T[j-1]. If they are different, then S[i-1] cannot be used to match T[j-1], so we must ignore it: dp[i][j] = dp[i-1][j]. If they are equal, we have two choices: either we use S[i-1] to match T[j-1], which contributes dp[i-1][j-1] ways, or we skip S[i-1] entirely, which contributes dp[i-1][j] ways. Therefore, dp[i][j] = dp[i-1][j-1] + dp[i-1][j] when characters match.

This recurrence captures the combinatorial explosion correctly because it enumerates all index subsets without double counting. The addition is disjoint: the set of subsequences that use position i-1 and those that do not are mutually exclusive. This is why the count can grow exponentially — for S = "aaaaa" and T = "aa", the answer is C(5,2) = 10 — and why we often compute the result modulo 10^9+7 in practice.

The DP Table Intuition

Visualize a grid with n+1 rows for prefixes of S and m+1 columns for prefixes of T. We fill it row by row. The first row and column are base cases. Each inner cell depends only on the cell directly above it and the cell diagonally above-left. This locality is what enables space optimization to a single dimension. The dependency graph is a DAG, so a bottom-up tabulation avoids recursion overhead and repeated computation.

An alternative viewpoint is as a counting version of the classic subsequence existence check. The existence check uses a boolean DP with OR logic, while the counting version replaces OR with addition. Similarly, longest common subsequence uses max, and edit distance uses min. Recognizing this pattern helps you derive recurrences for many string DP problems quickly.

3. Implementation

We will implement four progressively optimized solutions for distinct subsequences count. All examples count occurrences of T in S and handle large answers with optional modulo.

Naive Recursive Solution (Exponential)

This implementation directly translates the recurrence and is useful for understanding but will time out for strings longer than 30 characters due to overlapping subproblems.

def count_distinct_recursive(s, t, i, j):
    # Count distinct subsequences of t[:j] in s[:i]
    # i = len prefix of s, j = len prefix of t
    if j == 0:
        # empty target: one way (delete all)
        return 1
    if i == 0:
        # empty source but non-empty target: zero ways
        return 0
    if s[i-1] == t[j-1]:
        # use s[i-1] or skip it
        return count_distinct_recursive(s, t, i-1, j-1) + count_distinct_recursive(s, t, i-1, j)
    else:
        # must skip s[i-1]
        return count_distinct_recursive(s, t, i-1, j)

# Example: count_distinct_recursive("rabbbit", "rabbit", 7, 6) returns 3

Memoized Top-Down DP

Adding memoization reduces the complexity from exponential to polynomial by caching results for each (i, j) pair. This is often the most intuitive DP to write in an interview.

def count_distinct_memo(s, t):
    n, m = len(s), len(t)
    memo = {}

    def dp(i, j):
        # returns count for s[:i] and t[:j]
        if j == 0:
            return 1
        if i == 0:
            return 0
        if (i, j) in memo:
            return memo[(i, j)]
        if s[i-1] == t[j-1]:
            # characters match: use or skip
            val = dp(i-1, j-1) + dp(i-1, j)
        else:
            # characters differ: must skip
            val = dp(i-1, j)
        memo[(i, j)] = val
        return val

    return dp(n, m)

# Handles n,m up to ~500 with recursion limit increase
# For large alphabets, use lru_cache for cleaner code

Bottom-Up 2D Tabulation

The iterative version eliminates recursion depth issues and is the standard expected solution. It builds a (n+1) x (m+1) table explicitly.

def count_distinct_2d(s, t, mod=None):
    n, m = len(s), len(t)
    # dp[i][j] = count for s[:i] and t[:j]
    dp = [[0] * (m + 1) for _ in range(n + 1)]

    # base case: empty t
    for i in range(n + 1):
        dp[i][0] = 1

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if s[i-1] == t[j-1]:
                dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
            else:
                dp[i][j] = dp[i-1][j]
            if mod is not None:
                dp[i][j] %= mod
            # optimization: if j > i, dp[i][j] stays 0 because t longer than s
    return dp[n][m]

# Example:
# s = "babgbag", t = "bag"
# count_distinct_2d(s, t) == 5

Space-Optimized 1D DP

Since dp[i][j] only depends on the previous row dp[i-1][*], we can compress the table to a single array and iterate j backwards to avoid overwriting values needed for the next computation.

def count_distinct_1d(s, t, mod=10**9+7):
    n, m = len(s), len(t)
    if m > n:
        return 0
    # dp[j] = count for current prefix of s and t[:j]
    dp = [0] * (m + 1)
    dp[0] = 1  # empty t

    for i in range(1, n + 1):
        # iterate backwards so dp[j-1] is from previous i
        for j in range(m, 0, -1):
            if s[i-1] == t[j-1]:
                dp[j] = (dp[j] + dp[j-1]) % mod if mod else dp[j] + dp[j-1]
            # else dp[j] unchanged (skip s[i-1])
        # dp[0] remains 1
    return dp[m]

# Most efficient for production: O(n*m) time, O(m) space
# Backwards iteration is critical; forward would double count

Note that the 1D optimization is not just a memory trick — it improves cache locality and reduces allocation pressure, which matters when processing millions of strings in a web server or database engine. The backwards iteration ensures that dp[j-1] still holds the value from the previous row i-1 when we update dp[j].

4. Step-by-Step Example

Let us walk through a concrete example: S = "babgbag" and T = "bag". The answer should be 5. We will trace the 2D DP table to see how the count accumulates.

We initialize a table with 8 rows (0..7 for S) and 4 columns (0..3 for T). The column j=0 is all 1s, the row i=0, j>0 is all 0s. Now we fill row by row.

i=1, S[0]='b': For j=1, T[0]='b' match, so dp[1][1] = dp[0][0] + dp[0][1] = 1 + 0 = 1. For j=2, T[1]='a' no match, so dp[1][2] = dp[0][2] = 0. Similarly dp[1][3]=0.

i=2, S[1]='a': j=1 no match ('a' != 'b') so dp[2][1]=dp[1][1]=1. j=2 match ('a'=='a') so dp[2][2]=dp[1][1]+dp[1][2]=1+0=1. j=3 no match so dp[2][3]=0.

i=3, S[2]='b': dp[3][1]=dp[2][0]+dp[2][1]=1+1=2 (two 'b's so far). dp[3][2]=dp[2][2]=1 (no 'a' here). dp[3][3]=0.

i=4, S[3]='g': dp[4][1]=2, dp[4][2]=1, dp[4][3]=dp[3][2]+dp[3][3]=1+0=1 (first 'g' completes one "bag").

i=5, S[4]='b': dp[5][1]=3 (three 'b's), dp[5][2]=1, dp[5][3]=1.

i=6, S[5]='a': dp[6][1]=3, dp[6][2]=dp[5][1]+dp[5][2]=3+1=4 (new 'a' combines with three prior 'b's), dp[6][3]=1.

i=7, S[6]='g': dp[7][1]=3, dp[7][2]=4, dp[7][3]=dp[6][2]+dp[6][3]=4+1=5. The final answer dp[7][3]=5 enumerates all index combinations: (0,1,3), (0,1,6), (2,1,3), (2,1,6), (4,5,6) using 0-based indices of S.

Visual Trace Summary

The key insight from the trace is how matches propagate. Each time we see a character that matches the next needed character in T, we add the number of ways to form the prefix before it. This is why the counts can jump dramatically — the new 'a' at position 5 added 3 new ways to form "ba" because there were 3 ways to form "b" before it.

If we used the 1D optimized array, the same computation would happen in place: dp = [1,0,0,0] initially, after processing "b" becomes [1,1,0,0], after "a" becomes [1,1,1,0], and so on, with backwards updates preventing overwriting.

5. Complexity Analysis

Complexity analysis for distinct subsequences count depends heavily on the implementation strategy. The naive recursive solution without memoization explores two branches at each match, leading to exponential time. Memoization and tabulation both reduce this to polynomial time by ensuring each state (i,j) is computed once.

Time complexity is dominated by the nested loops over n = len(S) and m = len(T). For the DP solutions, we perform O(1) work per cell, so total time is O(n * m). This is optimal in the worst case because the answer can depend on all character pairs. However, if the alphabet is large and matches are sparse, we can optimize by preprocessing next-occurrence arrays or using automaton techniques, though the worst-case bound remains.

Space complexity varies. The full 2D table uses O(n * m) integers, which can be prohibitive for strings of length 10,000 (100M cells). The 1D optimization reduces this to O(m), and if m > n we can early exit with 0. For the variant that counts all distinct subsequences of a single string, we need an additional O(|alphabet|) array to store last occurrence positions.

Best, average, and worst cases are often the same for the DP tabulation because we fill the entire table regardless of character distribution. The only early exit is when m > n. Memoized recursion can be faster in the average case if mismatches prune many branches, but its worst case is still O(n*m).

Approach Best Case Time Average Case Time Worst Case Time Space Complexity
Naive Recursion (no memo) O(2^n) O(2^n) O(2^n) O(n + m) stack
Memoized Recursion (top-down) O(n * m) O(n * m) O(n * m) O(n * m) + O(n + m) stack
2D Tabulation (bottom-up) O(n * m) O(n * m) O(n * m) O(n * m)
1D Optimized DP O(n * m) O(n * m) O(n * m) O(m)
1D with Early Exit (m > n) O(1) O(n * m) O(n * m) O(m)

For large inputs where the count exceeds 64-bit integers, we usually compute modulo 1e9+7. This does not change complexity but requires careful handling of addition to avoid overflow in languages like Python (which has big ints) versus Java/C++ where overflow is a real risk. In Python, the cost of big integer arithmetic can make the effective time slightly superlinear for huge counts, but modulo keeps numbers bounded.

6. Variations and Optimizations

Several important variations of distinct subsequences count appear in practice, each with its own optimization opportunities.

Counting All Distinct Subsequences of a Single String

Here we count how many distinct strings (including empty) are subsequences of S itself, where duplicate characters cause duplicates. For example, S="aba" has 7 distinct subsequences: "", "a", "b", "ab", "ba", "aa", "aba" — note "a" appears twice via different indices but is counted once. The recurrence is dp[i] = 2*dp[i-1] - dp[last[S[i-1]]-1] if the character has appeared before, otherwise dp[i] = 2*dp[i-1]. This subtracts duplicates that were already counted before the previous occurrence.

Modulo and Large Number Handling

Counts can be astronomically large. For S of length 1000 consisting of all 'a's and T of length 500 of 'a's, the answer is C(1000,500) ~ 10^299. Always clarify whether the interviewer expects the exact count (Python big ints okay) or modulo 10^9+7. When using modulo, the subtraction in the single-string variant must be handled as (dp[i] - dp[last] + mod) % mod to avoid negative values.

Alphabet and Sparse Optimization

If the alphabet is small (e.g., DNA with 4 letters) and strings are long, we can preprocess for each position the next occurrence of each character to jump quickly to matches, similar to subsequence automaton. This is useful when answering many queries of different T against the same large S — we can build an automaton in O(n * |alphabet|) and answer each query in O(m) for existence, but counting still requires DP.

Bitset and Parallel Optimization

For counting existence (not number), bitset DP can speed up by 64x using bitwise operations. For counting, we can use divide-and-conquer with polynomial multiplication if we need to count subsequences by length distribution, but this is rarely needed in interviews.

Another practical optimization is to compress the DP to use array('I') or numpy for memory efficiency when n,m are up to 5000. In Python, using lists of ints is fine up to ~2000; beyond that, PyPy may need faster loops or numba JIT compilation.

7. Real-World Applications

While distinct subsequences count is often taught as an abstract DP exercise, its underlying pattern matching logic powers many real systems.

Bioinformatics and Genomics

DNA and protein sequences are analyzed for subsequences that represent functional motifs. Counting how many times a motif appears as a subsequence (not substring) helps estimate the statistical significance of a match. For example, counting distinct embeddings of a short primer in a long genome helps design PCR experiments and assess off-target binding. The DP for distinct subsequences is a simplified version of the algorithms used in sequence alignment tools like BLAST, which count gapped alignments.

Database and Information Retrieval

In fuzzy string matching and autocomplete, a query like "abc" might match database entries like "a_b_c", "ab_c", "a_bc" as subsequences. Counting the number of embeddings provides a relevance score — more embeddings often means a stronger match or more ways the query can be satisfied. Search engines use subsequence counting to rank typo-tolerant matches and to implement subsequence-based indexing for log analysis.

Web Servers and Network Security

Intrusion detection systems inspect network packet payloads for malicious subsequences that may be obfuscated by inserting junk bytes. Counting distinct subsequence embeddings helps detect polymorphic worms that insert variable gaps between signature bytes. Similarly, web application firewalls count how many ways a suspicious pattern (like "