115. Distinct Subsequences¶
Intuition¶
Scan s once and ask, for each character, the only question that matters: is it
used to match the next needed character of t, or skipped? That gives a count
over prefixes. Let dp[i][j] be the number of distinct subsequences of the first
i characters of s that spell the first j characters of t:
$$dp[i][j] = dp[i-1][j] + \begin{cases} dp[i-1][j-1] & s[i-1] = t[j-1] \ 0 & \text{otherwise} \end{cases}$$
The first term skips s[i-1]; the second consumes it to match t[j-1], which is
only legal when the characters agree. The base case is dp[i][0] = 1 — there is
exactly one way to spell the empty string, by taking nothing — and dp[0][j] = 0
for j > 0, since an empty source spells nothing.
Row i only ever reads row i - 1, so the table collapses to a single array
indexed by j, updated in place while i sweeps over s.
Approach: 1-D DP over t, Sweeping j Downward¶
- Allocate
dpof lengthn + 1withdp[0] = 1and the rest0. The invariant is that after processingicharacters ofs,dp[j]holds the number of ways to spellt[0..j)froms[0..i). - For each character
s[i-1], walkjfromndown to1. Ifs[i-1] == t[j-1], dodp[j] += dp[j-1]. - Return
dp[n].
The "skip" branch needs no code at all: leaving dp[j] untouched is carrying
dp[i-1][j] forward. Only the matching branch adds anything.
Why j must descend¶
This is the one detail that makes or breaks the 1-D version. The update needs
dp[j-1] as it stood before the current character was considered — the value
from row i - 1. Descending j reads each cell before that cell is itself
rewritten, so the row-i-1 values are still intact.
Ascending j would overwrite dp[j-1] first and then read the fresh row-i
value, letting a single character of s match several positions of t at once.
The damage shows up on the smallest possible input:
s |
t |
descending (correct) | ascending (wrong) |
|---|---|---|---|
"aa" |
"aa" |
1 |
3 |
"rabbbit" |
"rabbit" |
3 |
6 |
With s = "aa" and t = "aa" the ascending sweep already reports 1 after
reading a single a, having used that one character for both positions of t.
Worked example: s = "babgbag", t = "bag" → 5¶
dp starts as [1, 0, 0, 0], the entries standing for "", "b", "ba",
"bag".
| step | s[i-1] |
update | dp after |
|---|---|---|---|
| 1 | b |
dp[1] += dp[0] |
[1, 1, 0, 0] |
| 2 | a |
dp[2] += dp[1] |
[1, 1, 1, 0] |
| 3 | b |
dp[1] += dp[0] |
[1, 2, 1, 0] |
| 4 | g |
dp[3] += dp[2] |
[1, 2, 1, 1] |
| 5 | b |
dp[1] += dp[0] |
[1, 3, 1, 1] |
| 6 | a |
dp[2] += dp[1] |
[1, 3, 4, 1] |
| 7 | g |
dp[3] += dp[2] |
[1, 3, 4, 5] |
The answer is dp[3] = 5. Reading the last two steps backwards explains the
count: the final g extends all 4 ways of spelling "ba", and one "bag" was
already completed by the earlier g at step 4 — but that earlier g could only
draw on the single "ba" that existed at the time.
Overflow: what the 32-bit guarantee does and does not cover¶
The problem promises the answer fits in a signed 32-bit integer. It says
nothing about the intermediate cells, and those can be astronomically larger. For
s = "a" * 1000 and t = "a" * 999 — a perfectly legal input whose answer is
just 1000 — the cell dp[500] reaches $$\binom{1000}{500} \approx 10^{299}$$.
No fixed-width integer type holds that.
It turns out not to matter, and the reason is structural: the recurrence is
addition only. No subtraction, comparison, or division ever inspects a
partial sum. Two's-complement addition is exact modulo $$2^{32}$$, so every cell
stays congruent to its true value, and dp[n] comes out congruent to the true
answer modulo $$2^{32}$$. Since that answer is guaranteed to be a non-negative
value below $$2^{31}$$, the congruence pins it exactly. Wrapping is harmless
here.
The one build where it does bite is a debug Rust build, where arithmetic
overflow is a panic rather than a wrap: the input above aborts with attempt to
add with overflow. LeetCode compiles Rust with optimisations, so submissions
wrap and pass — but running the same code locally under cargo run without
--release will crash on adversarial input. Go's int is 64-bit and wraps
silently, so it is unaffected in practice, and Python's integers are unbounded,
so the question never arises.
Complexity¶
- Time complexity: $$O(m \cdot n)$$, where
mis the length ofsandnthe length oft— at most $$10^6$$ character comparisons under the constraints. - Space complexity: $$O(n)$$ for the single rolling row, down from $$O(m \cdot n)$$ for the full table.
Code¶
Go¶
func numDistinct(s string, t string) int {
m, n := len(s), len(t)
if m < n {
return 0
}
dp := make([]int, n + 1)
dp[0] = 1
for i := range m {
for j := n - 1; j >= 0; j-- {
if s[i] == t[j] {
dp[j+1] += dp[j]
}
}
}
return dp[n]
}
Two small departures from the other two versions. The m < n guard is a pure
shortcut — the DP already returns 0 when t is longer than s, because no
sweep can ever reach dp[n]. And the loops are 0-based, so the update reads
dp[j+1] += dp[j]; it is the same walk over the same pairs. Note for i := range m
is the Go 1.22 integer-range form; on an older toolchain write
for i := 0; i < m; i++.
Rust¶
impl Solution {
pub fn num_distinct(s: String, t: String) -> i32 {
let (s, t) = (s.as_bytes(), t.as_bytes());
let (m, n) = (s.len(), t.len());
let mut dp = vec![0; n + 1];
dp[0] = 1;
for i in 1..=m {
for j in (1..=n).rev() {
if s[i-1] == t[j-1] {
dp[j] += dp[j-1];
}
}
}
dp[n]
}
}
as_bytes avoids the cost of chars() on a String: the constraints say English
letters, so byte comparison and character comparison agree. The element type of
dp is inferred as i32 from the return of dp[n].
Python¶
class Solution:
def numDistinct(self, s: str, t: str) -> int:
m, n = len(s), len(t)
dp = [0] * (n + 1)
dp[0] = 1
for i in range(1, m+1):
for j in range(n, 0, -1):
if s[i-1] == t[j-1]:
dp[j] += dp[j-1]
return dp[n]
Test cases¶
s |
t |
answer | why |
|---|---|---|---|
"rabbbit" |
"rabbit" |
3 |
Example 1 |
"babgbag" |
"bag" |
5 |
Example 2 |
"aa" |
"aa" |
1 |
catches an ascending j sweep |
"abc" |
"abcd" |
0 |
t longer than s |
"abc" |
"d" |
0 |
no character of t occurs in s |
"abc" |
"abc" |
1 |
s and t identical |
"a" * 1000 |
"a" |
1000 |
every position matches |
"a" * 1000 |
"a" * 999 |
1000 |
answer fits, intermediates hit 10^299 |
All three implementations were checked against a big-integer reference DP on a
shared corpus: the two examples, all 620 pairs of non-empty binary strings up to
length 4 with |t| <= |s|, 400 random small inputs over alphabets of one to ten
letters, and 36 large inputs up to the maximum |s| = |t| = 1000. Of the 1058
generated cases, 1039 have an answer that fits in 32 bits — the rest would violate
the problem's own guarantee — and all three agreed with the reference on every one
of them. The reference was itself validated against a brute force enumerating
subsequence index sets, on every case short enough to enumerate.
The Rust build used for that run was a debug build, where overflow panics rather
than wraps, and it completed the 1036 cases whose intermediates fit in i32
without panicking. Run on s = "a" * 1000, t = "a" * 999 it does panic, while the
optimised build returns the correct 1000 — the wrapping argument above, observed
directly.