title: Distinct Subsequences
slug: distinct-subsequences
difficulty: hard
leetcode_id: 115
leetcode_url: https://leetcode.com/problems/distinct-subsequences/
categories:
  - strings
  - dynamic-programming
patterns:
  - dynamic-programming

description: |
  Given two strings `s` and `t`, return *the number of distinct **subsequences** of* `s` *which equals* `t`.

  A **subsequence** of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.

  For example, `"ace"` is a subsequence of `"abcde"` while `"aec"` is not.

  The test cases are generated so that the answer fits on a 32-bit signed integer.

constraints: |
  - `1 <= s.length, t.length <= 1000`
  - `s` and `t` consist of English letters

examples:
  - input: 's = "rabbbit", t = "rabbit"'
    output: "3"
    explanation: "There are 3 ways to form 'rabbit' from 'rabbbit' by choosing different combinations of the three 'b' characters."
  - input: 's = "babgbag", t = "bag"'
    output: "5"
    explanation: "There are 5 ways to form 'bag' from 'babgbag' by choosing different combinations of 'b', 'a', and 'g' characters."

explanation:
  intuition: |
    Imagine you're trying to "highlight" characters in string `s` such that the highlighted characters spell out `t`, maintaining their original order.

    The key insight is that this is fundamentally a **counting problem with choices**. When we encounter a character in `s` that matches the current character we need in `t`, we have two options:
    - **Use it**: Include this character as part of our subsequence match
    - **Skip it**: Don't use this character, hoping to find another match later

    Think of it like this: if you're looking for the word "cat" in "caat", when you reach the first 'a', you can either use it (and look for 't' next) or skip it (still looking for 'a'). Both paths might lead to valid subsequences.

    This **"use or skip"** decision at each matching character creates a branching structure that screams **dynamic programming**. We need to count all possible ways, not just find one, which is why we sum up the counts from both choices.

    The DP state naturally becomes: "How many ways can we match the first `j` characters of `t` using the first `i` characters of `s`?"

  approach: |
    We solve this using **2D Dynamic Programming**:

    **Step 1: Define the DP state**

    - `dp[i][j]`: The number of distinct subsequences of `s[0...i-1]` that equal `t[0...j-1]`
    - We use 1-indexed DP for cleaner base cases (index 0 represents empty string)

    &nbsp;

    **Step 2: Establish base cases**

    - `dp[i][0] = 1` for all `i`: There's exactly one way to form an empty string — delete all characters
    - `dp[0][j] = 0` for all `j > 0`: Cannot form a non-empty string from an empty source

    &nbsp;

    **Step 3: Fill the DP table**

    - For each position `(i, j)`, if `s[i-1] == t[j-1]` (characters match):
      - `dp[i][j] = dp[i-1][j-1] + dp[i-1][j]`
      - We can either **use** this character (`dp[i-1][j-1]`) or **skip** it (`dp[i-1][j]`)
    - If characters don't match:
      - `dp[i][j] = dp[i-1][j]`
      - We must skip this character in `s`

    &nbsp;

    **Step 4: Return the result**

    - Return `dp[m][n]` where `m = len(s)` and `n = len(t)`

    &nbsp;

    The recurrence captures the essence of the problem: at each step, we inherit all ways from skipping, plus (if characters match) all ways from using the current character.

  common_pitfalls:
    - title: Recursive Without Memoisation
      description: |
        A naive recursive solution tries all possibilities but recalculates the same subproblems repeatedly.

        With `s.length` and `t.length` up to 1000, the exponential branching factor means a plain recursive solution will **Time Limit Exceed (TLE)**.

        Always use memoisation or bottom-up DP to store computed results.
      wrong_approach: "Plain recursion exploring all paths"
      correct_approach: "Memoised recursion or bottom-up DP table"

    - title: Incorrect Base Case
      description: |
        A common mistake is setting `dp[0][0] = 0` or forgetting that an empty pattern `t` can always be formed from any string `s` (by selecting no characters).

        The correct base case is `dp[i][0] = 1` for all `i` — there's exactly one way to form an empty subsequence.
      wrong_approach: "dp[i][0] = 0 or inconsistent base cases"
      correct_approach: "dp[i][0] = 1 for all i (one way to form empty string)"

    - title: Off-by-One Errors with Indexing
      description: |
        When using 1-indexed DP (which simplifies base cases), remember that `dp[i][j]` refers to the first `i` characters of `s` and first `j` characters of `t`.

        When comparing characters, use `s[i-1]` and `t[j-1]`, not `s[i]` and `t[j]`.
      wrong_approach: "Comparing s[i] with t[j] in 1-indexed DP"
      correct_approach: "Compare s[i-1] with t[j-1] when using 1-indexed DP"

    - title: Forgetting to Handle Integer Overflow
      description: |
        With large inputs, the number of subsequences can grow extremely large. The problem guarantees the answer fits in a 32-bit signed integer, but intermediate DP values might overflow in some languages.

        In Python this isn't an issue due to arbitrary precision integers, but in C++/Java you may need modular arithmetic or careful overflow handling.

  key_takeaways:
    - "**Classic DP pattern**: The 'use or skip' decision structure appears in many string matching problems like edit distance and longest common subsequence"
    - "**State definition is key**: `dp[i][j]` counting ways to match `t[0...j-1]` using `s[0...i-1]` naturally captures the subproblem structure"
    - "**Space optimisation possible**: Since each row only depends on the previous row, you can reduce space from O(m*n) to O(n) using a 1D array (iterate right-to-left)"
    - "**Foundation for harder problems**: This pattern extends to problems involving counting paths, combinations, and string transformations"

  time_complexity: "O(m * n). We fill a 2D DP table of size `m * n` where `m = len(s)` and `n = len(t)`, with O(1) work per cell."
  space_complexity: "O(m * n) for the 2D DP table. Can be optimised to O(n) using a 1D array since each row only depends on the previous row."

solutions:
  - approach_name: 2D Dynamic Programming
    is_optimal: true
    code: |
      def num_distinct(s: str, t: str) -> int:
          m, n = len(s), len(t)

          # dp[i][j] = number of ways to form t[0...j-1] from s[0...i-1]
          dp = [[0] * (n + 1) for _ in range(m + 1)]

          # Base case: empty t can be formed from any prefix of s (one way)
          for i in range(m + 1):
              dp[i][0] = 1

          # Fill the DP table
          for i in range(1, m + 1):
              for j in range(1, n + 1):
                  # Always inherit ways from skipping s[i-1]
                  dp[i][j] = dp[i - 1][j]

                  # If characters match, add ways from using s[i-1]
                  if s[i - 1] == t[j - 1]:
                      dp[i][j] += dp[i - 1][j - 1]

          return dp[m][n]
    explanation: |
      **Time Complexity:** O(m * n) — We iterate through each cell of the m x n DP table once.

      **Space Complexity:** O(m * n) — We store the full 2D DP table.

      The solution builds up the count of subsequences by considering each character in `s` and deciding whether to use it (if it matches) or skip it. The recurrence relation captures both choices and sums their contributions.

  - approach_name: Space-Optimised 1D DP
    is_optimal: true
    code: |
      def num_distinct(s: str, t: str) -> int:
          m, n = len(s), len(t)

          # 1D array: dp[j] = ways to form t[0...j-1]
          dp = [0] * (n + 1)
          dp[0] = 1  # One way to form empty string

          # Process each character in s
          for i in range(1, m + 1):
              # Iterate right-to-left to avoid using updated values
              for j in range(n, 0, -1):
                  if s[i - 1] == t[j - 1]:
                      # Add ways from using current character
                      dp[j] += dp[j - 1]
              # dp[j] already inherits previous value (skip case)

          return dp[n]
    explanation: |
      **Time Complexity:** O(m * n) — Same iteration as 2D approach.

      **Space Complexity:** O(n) — Only one 1D array of length `n + 1`.

      Since each row of the 2D DP only depends on the previous row, we can use a single array. The key insight is iterating right-to-left so we don't overwrite values we still need. The "skip" case is handled automatically since `dp[j]` retains its value from the previous iteration.

  - approach_name: Recursive with Memoisation
    is_optimal: false
    code: |
      def num_distinct(s: str, t: str) -> int:
          from functools import lru_cache

          @lru_cache(maxsize=None)
          def count(i: int, j: int) -> int:
              # Base case: matched all of t
              if j == len(t):
                  return 1
              # Base case: exhausted s but t remains
              if i == len(s):
                  return 0

              # Skip s[i]
              result = count(i + 1, j)

              # Use s[i] if it matches t[j]
              if s[i] == t[j]:
                  result += count(i + 1, j + 1)

              return result

          return count(0, 0)
    explanation: |
      **Time Complexity:** O(m * n) — Each unique state (i, j) is computed once due to memoisation.

      **Space Complexity:** O(m * n) for the memoisation cache, plus O(m + n) for the recursion stack.

      This top-down approach is more intuitive: at each position, we either skip the current character in `s` or use it (if it matches). Memoisation ensures we don't recompute the same subproblems. The bottom-up approach is generally preferred for better constant factors and no recursion overhead.
