DSAPrep
Hard2-D DP

Distinct Subsequences

Given two strings s and t, return the number of distinct subsequences of s which equals t.

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

Example 1

Input: s = "rabbbit", t = "rabbit"
Output: 3
Explanation: There are 3 ways to generate "rabbit" from s by deleting different combinations of the extra b's.

Example 2

Input: s = "babgbag", t = "bag"
Output: 5
Explanation: There are 5 ways to generate "bag" from s.

Constraints

  • 1 <= s.length, t.length <= 1000
  • s and t consist of English letters.
View original on LeetCode ↗

Walk s and t together. At each character of s, either it is used to match the current character of t (only possible if they are equal), or it is skipped over. Both choices can be valid at once when the characters match — skipping still counts, because a later matching occurrence of the same character in s might be needed for a different subsequence.

Brute Force Recursion

Time O(2^m)Space O(m + n)

At index i in s and j in t: if s[i] == t[j], count both “use this character” (advance both pointers) and “skip it” (advance only i). If they differ, the only option is to skip.

class Solution:
def numDistinct(self, s: str, t: str) -> int:
m, n = len(s), len(t)
def rec(i: int, j: int) -> int:
if j == n:
return 1
if i == m:
return 0
skip = rec(i + 1, j)
take = rec(i + 1, j + 1) if s[i] == t[j] else 0
return skip + take
return rec(0, 0)

Every character of s can independently be used or skipped, and the same (i, j) pair recurs across many branches — exponential blow-up.

2-D DP Table (Bottom-Up)

OptimalTime O(m·n)Space O(m·n)

dp[i][j] is the number of ways to form t[0:j] as a subsequence of s[0:i]. Column 0 (t empty) is always 1 — the empty subsequence is formed exactly one way, by skipping everything. Row 0 (s empty, t non-empty) is always 0. Every other cell always inherits “skip s[i-1]” (dp[i-1][j]), and additionally adds “match s[i-1] to t[j-1]” (dp[i-1][j-1]) when the characters agree.

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]

Filling the table for s = "rara", t = "ra" (a small illustrative case):

1
0
1
·
1
·
1
·
1
·
1 / 5
comparingseenresult

Base cases: column 0 (empty t) is always 1 way. Row 0 (empty s, non-empty t) is always 0 ways.

The remaining column (j=2, matching t="ra" in full) follows the same rule: dp[1][2]=0, dp[2][2]=dp[1][2]+dp[1][1]=1, dp[3][2]=dp[2][2]=1, and finally dp[4][2]=dp[3][2]+dp[3][1]=1+2=3. There are 3 ways to spell ra from rara: positions (r@0,a@1), (r@0,a@3), and (r@2,a@3).

Complexity: each of the (m+1)·(n+1) cells does O(1) work → O(m·n) time, O(m·n) space.

1-D Rolling Array

Time O(m·n)Space O(n)

Row i reads only from row i-1, at columns j and j-1. Iterating columns right to left within a single reused row lets dp[j-1] still hold last row’s value when it’s read (it hasn’t been overwritten yet this pass), while dp[j] gets updated in place.

class Solution:
def numDistinct(self, s: str, t: str) -> int:
n = len(t)
dp = [1] + [0] * n
for ch in s:
for j in range(n, 0, -1):
if ch == t[j - 1]:
dp[j] += dp[j - 1]
return dp[n]

Same O(m·n) time, but only one row of length n + 1 is kept — O(n) space.