Appearance
4.24.8 — Distinct Subsequences
LeetCode 115 · Hard
The problem
Count how many distinct subsequences of s equal t. Subsequences at different positions count separately.
s = "rabbbit", t = "rabbit" → 3
s = "babgbag", t = "bag" → 5Constraints:
1 <= s.length, t.length <= 1000sandtconsist of English letters.- The answer is guaranteed to fit in a 32-bit integer.
Plain-English Restatement: Given a source string s and a target string t, how many different ways can you cross out characters in s to be left with exactly t?
Brute Force Approach: Generate every possible subsequence of s (by deciding to keep or delete each character) and count how many exactly match t.
- Time Complexity: O(2^M \cdot N). A string of length
Mhas 2^M subsequences. - Why it's wasteful: We'll repeatedly check identical prefixes. DP avoids this by building up the counts of valid prefixes incrementally.
In the first example the three bs give three ways to pick the two bs that "rabbit" needs.
The pattern
Two strings, so a grid — the recognition step from 4.24.2.
Let dp[i][j] be the number of ways the first i characters of s can produce the first j characters of t.
The recurrence asks what you do with s[i-1], the character at the end of the s prefix. There are two cases, and the first one is where people go wrong.
If s[i-1] == t[j-1] you have a choice, and both options are legal:
- Use it to match
t[j-1], leavingdp[i-1][j-1]ways. - Skip it, and match
t[j]from the earlier part ofsinstead, leavingdp[i-1][j]ways.
Both are counted, so they add:
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
If they differ, s[i-1] is useless here, so you can only skip it:
dp[i][j] = dp[i-1][j]
The + in the match case is the whole problem. In 4.24.2 a match forced you to take the character; here it only offers the option, and counting means both branches contribute.
The base cases
dp[i][0] = 1 for every i. There is exactly one way to produce the empty string from any prefix of s — delete everything. This is the seed the whole table is built from, and setting it to 0 makes every answer 0.
dp[0][j] = 0 for j > 0. An empty s cannot produce a non-empty t.
The solution
python
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 # one way to make the empty string
for i in range(1, m + 1):
for j in range(1, n + 1):
dp[i][j] = dp[i - 1][j] # skip s[i-1]
if s[i - 1] == t[j - 1]:
dp[i][j] += dp[i - 1][j - 1] # or use it
return dp[m][n]ts
function numDistinct(s: string, t: string): number {
const m = s.length, n = t.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) dp[i][0] = 1;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
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];
}Writing the skip case first and then adding the match case is cleaner than an if/else, because the skip is always available and the match is a bonus.
Trace
s = "babgbag", t = "bag". The table's last row builds up to 5, and the five subsequences are worth seeing:
b a b g b a g
b a . g . . . indices 0,1,3
b a . . . . g indices 0,1,6
b . . . . a g indices 0,5,6
. . b . . a g indices 2,5,6
. . . . b a g indices 4,5,6Each is a different set of positions, which is why they count separately.
Space optimisation
Each row reads only the row above, so one array suffices — but the direction matters:
python
dp = [0] * (n + 1)
dp[0] = 1
for i in range(1, m + 1):
for j in range(n, 0, -1): # BACKWARDS
if s[i-1] == t[j-1]:
dp[j] += dp[j-1]
return dp[n]The inner loop counts down, so that dp[j-1] still holds the previous row's value when it is read. Going forwards would use a value already updated in this row, which mixes rows and gives the wrong count.
This is the same direction rule as 4.23.12 and 4.24.4, and it appears here for the third time — whenever a rolled array reads a smaller index from the previous row, iterate downwards.
O(n) space.
Code Walkthrough
- Grid Init:
dp = [[0] * (n + 1) for _ in range(m + 1)]. - Base Case (Empty
t):for i in range(m + 1): dp[i][0] = 1. Any prefix ofscan form an empty stringtexactly 1 way: by deleting everything. The top rowdp[0][j]stays0because an emptyscannot form a non-emptyt. - Loops: Iterate
ifrom1tom, andjfrom1ton. - Skip Case:
dp[i][j] = dp[i - 1][j]. By default, we can always choose to not use the current characters[i-1]. The number of ways is at least the number of ways we formedt[:j]usings[:i-1]. - Use Case:
if s[i - 1] == t[j - 1]: dp[i][j] += dp[i - 1][j - 1]. If the characters match, we have an additional set of ways to formt: we take the match, and add the number of ways we formedt[:j-1]usings[:i-1].
Edge Cases
sis shorter thant: Handled correctly. The grid will process, buttcan never be fully formed. Returns 0.- Strings are identical: The diagonal will carry
1s all the way down, returning 1 (assuming no repeating characters).
Complexity
O(mn) time, O(mn) or O(n) space.
The counts can grow very large; the problem guarantees the answer fits in a 32-bit signed integer, which is worth noticing as a hint that no modular arithmetic is needed.
Where this goes next
- Longest Common Subsequence — the same grid,
maxinstead of+, and a match is forced rather than optional. 4.24.2. - Edit Distance — the same grid with three operations. 4.24.9.
- Is
ta subsequence ofs(LeetCode 392) — just whether, not how many, so two pointers do it in O(n) with no table. A good reminder that "does it exist" is often far cheaper than "how many".
What the interviewer will push on
"Why does the match case add two terms?" A matching character may be used or skipped, and both produce valid subsequences, so counting adds them.
"Why is dp[i][0] = 1?" One way to make the empty string — delete everything. Every count grows from that seed.
"Why does the rolled loop go backwards?" So dp[j-1] still holds the previous row.
"How does this differ from LCS?" LCS forces the match and takes a maximum; this offers the match and adds.
"What if you only needed to know whether t is a subsequence?" Two pointers, O(n), no DP at all.
One thing to volunteer: state the difference from LCS in one sentence before writing code. It is the same grid, and naming the one changed rule shows you are reusing a template rather than meeting a new problem.
Next: 4.24.9 Edit Distance — the most useful two-string DP there is.