118. Distinct Subsequences
View on NeetCode
View on LeetCode
Problem
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:
As shown below, there are 3 ways you can generate "rabbit" from s.
rabb**b**it
ra**b**bbit
rab**b**bit
Example 2:
Input: s = "babgbag", t = "bag"
Output: 5
Explanation:
As shown below, there are 5 ways you can generate "bag" from s.
**ba**b**g**bag
**ba**bgba**g**
**b**abgb**ag**
ba**b**g**bag**
babg**bag**
Constraints:
1 <= s.length, t.length <= 1000sandtconsist of English letters.
Solution
Approach: 2D Dynamic Programming
The key insight is that for each character in s, we decide whether to use it to match a character in t or skip it.
Implementation
class Solution:
def numDistinct(self, s: str, t: str) -> int:
m, n = len(s), len(t)
# dp[i][j] = number of ways to form t[0:j] from s[0:i]
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Base case: empty t can be formed in one way (delete all from s)
for i in range(m + 1):
dp[i][0] = 1
for i in range(1, m + 1):
for j in range(1, n + 1):
# Don't use s[i-1] to match
dp[i][j] = dp[i-1][j]
# Use s[i-1] to match t[j-1]
if s[i-1] == t[j-1]:
dp[i][j] += dp[i-1][j-1]
return dp[m][n]
Approach 2: Space-Optimized 1D DP
class Solution:
def numDistinct(self, s: str, t: str) -> int:
m, n = len(s), len(t)
# Only need previous row
prev = [0] * (n + 1)
prev[0] = 1
for i in range(1, m + 1):
curr = [0] * (n + 1)
curr[0] = 1
for j in range(1, n + 1):
# Don't use current character of s
curr[j] = prev[j]
# Use current character if it matches
if s[i-1] == t[j-1]:
curr[j] += prev[j-1]
prev = curr
return prev[n]
Approach 3: Recursion with Memoization
class Solution:
def numDistinct(self, s: str, t: str) -> int:
memo = {}
def dp(i, j):
# Base cases
if j == len(t):
return 1 # Matched all of t
if i == len(s):
return 0 # No more characters in s
if (i, j) in memo:
return memo[(i, j)]
# Don't use s[i]
result = dp(i + 1, j)
# Use s[i] if it matches t[j]
if s[i] == t[j]:
result += dp(i + 1, j + 1)
memo[(i, j)] = result
return result
return dp(0, 0)
Complexity Analysis
2D DP:
- Time Complexity: O(m * n), where m and n are lengths of s and t.
- Space Complexity: O(m * n) for the dp table.
1D DP:
- Time Complexity: O(m * n).
- Space Complexity: O(n), only storing two rows.
Key Insights
-
DP State:
dp[i][j]= number of distinct subsequences of s[0:i] that equal t[0:j]. - Two Choices at Each Position:
- Skip current character in s: dp[i-1][j]
- Use current character if it matches: dp[i-1][j-1]
-
Character Match: When s[i-1] == t[j-1], we add both choices.
- Base Cases:
- Empty t can always be formed (dp[i][0] = 1)
- Empty s cannot form non-empty t (dp[0][j] = 0 for j > 0)
-
Count All Ways: Unlike LCS which finds length, we count the number of different ways to form subsequences.
- Order Matters: We process s left to right, maintaining order required for subsequences.