TL;DR
Two-string DP: dp[i][j] counts how many ways t[:j] is a subsequence of s[:i] — O(len(s)·len(t)) time, O(len(t)) space with a rolling row.
Approach 1 — Brute force recursion
At each character of s you either skip it, or (if it matches the current t character) consume both. Count the paths that consume all of t.
class Solution:
def numDistinct(self, s: str, t: str) -> int:
def count(i: int, j: int) -> int:
if j == len(t):
return 1 # matched all of t
if i == len(s):
return 0 # ran out of s with t unfinished
ways = count(i + 1, j) # skip s[i]
if s[i] == t[j]:
ways += count(i + 1, j + 1) # use s[i] to match t[j]
return ways
return count(0, 0)
Complexity: O(2^len(s)) time in the worst case — every character forks into skip/use. Exponential and infeasible for length-1000 strings.
Approach 2 — Top-down memoization
The insight: the count from position (i, j) onward depends only on (i, j), not on the earlier choices that led there. There are only len(s)·len(t) such states, so caching removes the recomputation.
State / recurrence. count(i, j) = number of ways t[j:] appears as a subsequence of s[i:]. Transition count(i, j) = count(i+1, j) + (count(i+1, j+1) if s[i]==t[j] else 0); base cases count(i, len(t)) = 1 and count(len(s), j<len(t)) = 0.
from functools import lru_cache
class Solution:
def numDistinct(self, s: str, t: str) -> int:
@lru_cache(maxsize=None)
def count(i: int, j: int) -> int:
if j == len(t):
return 1
if i == len(s):
return 0
ways = count(i + 1, j)
if s[i] == t[j]:
ways += count(i + 1, j + 1)
return ways
return count(0, 0)
Complexity: O(len(s)·len(t)) time and space.
Approach 3 — Bottom-up tabulation (the 2-D table)
State / recurrence. dp[i][j] = number of ways the first j characters of t form a subsequence of the first i characters of s.
dp[i][0] = 1 # empty t: delete everything, one way
dp[0][j] = 0 for j > 0 # non-empty t from empty s: impossible
dp[i][j] = dp[i-1][j] # skip s[i-1]
+ dp[i-1][j-1] if s[i-1]==t[j-1] # also use it to match t[j-1]
class Solution:
def numDistinct(self, s: str, t: str) -> int:
m, n = len(s), len(t)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = 1
for i in range(1, m + 1):
for j in range(1, n + 1):
dp[i][j] = dp[i - 1][j]
if s[i - 1] == t[j - 1]:
dp[i][j] += dp[i - 1][j - 1]
return dp[m][n]
Walkthrough with s = "rabbbit", t = "rabbit". The r, a match uniquely, so counts stay 1 through the "ra" prefix. The three b’s in s against the two b’s in t are where the count grows: each extra b in s gives another way to have skipped exactly one of them. Matching "rab", then "rabb", the ways accumulate to 3 by the time i reaches the end of the b run, and the unique i, t tail carries 3 through to dp[7][6] = 3.
Complexity: O(m·n) time, O(m·n) space.
Approach 4 — Space-optimized rolling row
The insight: dp[i] reads only from dp[i-1]. Keep one array indexed by j and sweep j from high to low so dp[j-1] still holds the previous row’s value (the dp[i-1][j-1] term) when we update dp[j].
class Solution:
def numDistinct(self, s: str, t: str) -> int:
n = len(t)
dp = [0] * (n + 1)
dp[0] = 1
for ch in s:
for j in range(n, 0, -1):
if ch == t[j - 1]:
dp[j] += dp[j - 1]
return dp[n]
Complexity: O(m·n) time, O(n) space.
Common pitfalls
- Getting the base cases backwards:
dp[i][0] = 1 for every i (empty target matched by deleting all), but dp[0][j] = 0 for j > 0.
- In the rolling-row version, sweeping
j upward, which overwrites dp[j-1] with the current row before it is read — you must go downward.
- Adding the match term unconditionally; it applies only when
s[i-1] == t[j-1].
- Confusing this “count of subsequences” with “is
t a subsequence of s” (a boolean/greedy problem) — here every distinct index-set counts.
Pattern takeaway
Two-sequence DP indexes a table by a prefix of each string, dp[i][j], and each cell branches on whether the two current characters match. The recurring shape is “skip a character of the source (always) plus, on a match, an aligned diagonal term”. When one dimension only reads the immediately previous row, collapse to a single array — swept in the direction that preserves the diagonal dependency.