Skip to content

115. Distinct Subsequences (Hard)

Problem

Given two strings s and t, return the number of distinct subsequences of s that equal t. The answer fits in a 32-bit signed integer.

Example

  • s = "rabbbit", t = "rabbit"3
  • s = "babgbag", t = "bag"5

LeetCode 115 · Link · Hard

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Recursive

f(i, j) = number of ways t[j:] appears as a subsequence of s[i:].

  • If j == len(t): matched everything → 1.
  • If i == len(s): out of characters → 0.
  • If s[i] == t[j]: f(i + 1, j + 1) + f(i + 1, j) (use or skip).
  • Else: f(i + 1, j) (must skip).
def num_distinct(s, t):
def f(i, j):
if j == len(t): # L1: O(1) base: t fully matched
return 1
if i == len(s): # L2: O(1) base: s exhausted
return 0
if s[i] == t[j]: # L3: O(1) char match check
return f(i + 1, j + 1) + f(i + 1, j) # L4: two calls (use or skip)
return f(i + 1, j) # L5: one call (must skip)
return f(0, 0)

Where the time goes, line by line

Variables: m = len(s), n = len(t).

LinePer-call costTimes executedContribution
L1-L3 (base + match check)O(1)O(1)per callO(1)O(1) each
L4 (two recursive calls on match)O(1)O(1) work + 2 callsworst case every callO(2m)O(2^m) ← dominates
L5 (one recursive call on mismatch)O(1)O(1) work + 1 callper mismatchO(2m)O(2^m) same tree

In the worst case (s and t all the same character), every position in s can match t[j], spawning two branches each time. The call tree is exponential in m.

Complexity

  • Time: O(2n)O(2^n) in the worst case (here n = len(s)), driven by L4 double-branching.
  • Space: O(n)O(n) recursion depth.

Approach 2: Top-down memoized

from functools import lru_cache
def num_distinct(s, t):
@lru_cache(maxsize=None) # L1: cache decorator
def f(i, j):
if j == len(t): # L2: O(1) base case
return 1
if i == len(s): # L3: O(1) base case
return 0
if s[i] == t[j]: # L4: O(1) char match
return f(i + 1, j + 1) + f(i + 1, j) # L5: O(1) with cache
return f(i + 1, j) # L6: O(1) with cache
return f(0, 0)

Where the time goes, line by line

Variables: m = len(s), n = len(t).

LinePer-call costTimes executedContribution
L1 (lru_cache)O(1)O(1)1O(1)O(1)
L2-L4 (checks)O(1)O(1)once per unique (i,j)O(mn)O(m · n) total
L5, L6 (cached calls)O(1)O(1) per callat most (m+1)(n+1) statesO(mn)O(m · n) ← dominates

Each (i, j) pair is computed exactly once. There are (m+1) * (n+1) such pairs, each doing O(1)O(1) work.

Complexity

  • Time: O(mn)O(m · n), driven by L5/L6 across all unique (i,j) states.
  • Space: O(mn)O(m · n) for the memo table.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: Bottom-up 2-D DP + rolling to 1-D (optimal space)

dp[j] = number of ways t[:j] appears as a subsequence of the current prefix of s. Iterate j from high to low to avoid reusing updates within a single row.

def num_distinct(s, t):
m, n = len(s), len(t) # L1: O(1)
dp = [0] * (n + 1) # L2: O(n) 1-D table init
dp[0] = 1 # L3: O(1) base: empty t matched by any prefix
for i in range(m): # L4: outer loop over s O(m)
for j in range(n, 0, -1): # L5: inner loop over t, right-to-left O(n)
if s[i] == t[j - 1]: # L6: O(1) char match check
dp[j] += dp[j - 1] # L7: O(1) accumulate ways
return dp[n] # L8: O(1) answer

Where the time goes, line by line

Variables: m = len(s), n = len(t).

LinePer-call costTimes executedContribution
L1-L3 (init)O(n)O(n)1O(n)O(n)
L4+L5 (double loop)O(1)O(1) bodym · nO(mn)O(m · n) ← dominates
L6-L7 (update)O(1)O(1)at most m · nincluded above

The right-to-left inner loop is critical: it prevents an s[i] character from being used to extend two different t prefixes in the same outer iteration (same reason 0/1 knapsack iterates right-to-left).

Complexity

  • Time: O(mn)O(m · n), driven by L4/L5 (the double loop).
  • Space: O(n)O(n) for the rolling 1-D array.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Summary

ApproachTimeSpace
Naive recursionO(2n)O(2^n)O(n)O(n)
Top-down memoO(mn)O(m · n)O(mn)O(m · n)
1-D bottom-up DPO(mn)O(m · n)O(n)O(n)

Test cases

# Quick smoke tests, paste into a REPL or save as test_115.py and run.
# Uses the canonical implementation (Approach 3: 1-D bottom-up DP).
def num_distinct(s, t):
m, n = len(s), len(t)
dp = [0] * (n + 1)
dp[0] = 1
for i in range(m):
for j in range(n, 0, -1):
if s[i] == t[j - 1]:
dp[j] += dp[j - 1]
return dp[n]
def _run_tests():
# problem statement examples
assert num_distinct("rabbbit", "rabbit") == 3
assert num_distinct("babgbag", "bag") == 5
# edge: t is empty (one way: pick nothing)
assert num_distinct("abc", "") == 1
# edge: s is empty, t is not
assert num_distinct("", "a") == 0
# edge: s == t (exactly one way)
assert num_distinct("abc", "abc") == 1
# no match at all
assert num_distinct("aaa", "b") == 0
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Sequence DP, the prefix or position state pattern used for strings and ordered arrays.
  • Dynamic Programming, the state and transition model for reusing answers to overlapping subproblems.