118.Distinct Subsequences
Problem
Given two strings s and t, return the number of distinct subsequences of s that equal t.
Two subsequences are different if they use a different set of indices in s, even if the resulting strings look the same. The answer is guaranteed to fit in a 32-bit signed integer.
Examples
Input: s = "rabbbit", t = "rabbit"
Output: 3
Explanation: s has three 'b's; drop exactly one of them:
ra_bbit, rab_bit, rabb_it (the underscore marks the dropped 'b')Input: s = "babgbag", t = "bag"
Output: 5
Explanation: Index choices (b, a, g):
(0,1,3), (0,1,6), (0,5,6), (2,5,6), (4,5,6)Input: s = "abc", t = "abcd"
Output: 0
Explanation: t is longer than s.Constraints
1 <= s.length, t.length <= 1000sandtconsist of English letters.
💡 Key Insight
Walk through s one character at a time while trying to match t. At s[i] you can always skip it; if s[i] == t[j] you may also use it to match t[j]. The two options produce disjoint sets of index choices, so their counts simply add:
ways(i, j) = ways(i + 1, j) + (s[i] == t[j] ? ways(i + 1, j + 1) : 0).
Matching all of t counts as 1; running out of s first counts as 0. The table compresses to one row if j is iterated in the right direction.
Recursion (Brute Force)
TimeO(2^m)SpaceO(m)Intuition
ways(i, j) = number of ways s[i..] contains t[j..] as a subsequence. Apply skip/use at each character.
Algorithm
ways(i, j):- If
j == t.length(), return1(all oftmatched). - If
i == s.length(), return0. res = ways(i + 1, j)(skips[i]).- If
s[i] == t[j]:res += ways(i + 1, j + 1)(uses[i]).
- If
- Return
ways(0, 0).
Visualization
flowchart TD
A["ways(0,0)<br/>s='abb' t='ab'"] -->|"use a"| B["ways(1,1)<br/>'bb' vs 'b'"]
A -->|"skip a"| C["ways(1,0)<br/>'bb' vs 'ab' → 0"]
B -->|"use b"| D["ways(2,2) = 1"]
B -->|"skip b"| E["ways(2,1)<br/>'b' vs 'b'"]
E -->|"use b"| F["ways(3,2) = 1"]
E -->|"skip b"| G["ways(3,1) = 0"]
D --> H["total = 1 + 1 = 2"]
F --> HCode
class Solution {
public int numDistinct(String s, String t) {
return ways(s, t, 0, 0);
}
private int ways(String s, String t, int i, int j) {
if (j == t.length()) {
return 1; // matched all of t
}
if (i == s.length()) {
return 0; // ran out of s
}
int res = ways(s, t, i + 1, j); // skip s[i]
if (s.charAt(i) == t.charAt(j)) {
res += ways(s, t, i + 1, j + 1); // use s[i] for t[j]
}
return res;
}
}Complexity
- Time: O(2^m) — each character of
smay branch twice. - Space: O(m) — recursion depth.
Top-Down Memoization
TimeO(m · n)SpaceO(m · n)Intuition
Only m · n distinct (i, j) pairs exist. Cache them in an Integer[][] (use null as the sentinel — counts may wrap around in intermediate states, so -1 is not a safe marker).
Algorithm
memo = new Integer[m][n].- Same recursion with a cache lookup before computing.
Dry Run
s = "babgbag", t = "bag"
ways(5,1) 'ag' vs 'ag' = 1
ways(4,1) 'bag' vs 'ag' = ways(5,1) = 1
ways(1,1) 'abgbag' vs 'ag' = ways(2,1) + ways(2,2) = 1 + 2 = 3 (cached)
ways(0,0) = ways(1,0) + ways(1,1) = 2 + 3 = 5Code
class Solution {
private Integer[][] memo;
private String s, t;
public int numDistinct(String s, String t) {
this.s = s;
this.t = t;
memo = new Integer[s.length()][t.length()];
return ways(0, 0);
}
private int ways(int i, int j) {
if (j == t.length()) {
return 1;
}
if (i == s.length()) {
return 0;
}
if (memo[i][j] != null) {
return memo[i][j];
}
int res = ways(i + 1, j);
if (s.charAt(i) == t.charAt(j)) {
res += ways(i + 1, j + 1);
}
memo[i][j] = res;
return res;
}
}Complexity
- Time: O(m · n) — each state once.
- Space: O(m · n) — memo plus O(m) stack.
Bottom-Up 2-D Tabulation
TimeO(m · n).SpaceO(m · n).Intuition
Use prefixes: dp[i][j] = number of ways the first i chars of s contain the first j chars of t. The empty t is contained exactly once in any prefix (dp[i][0] = 1).
Recurrence: dp[i][j] = dp[i-1][j] + (s[i-1] == t[j-1] ? dp[i-1][j-1] : 0).
Algorithm
dp = new int[m + 1][n + 1];dp[i][0] = 1for everyi.- For
i = 1..m,j = 1..n, apply the recurrence. - Return
dp[m][n].
Dry Run
s = "rabbbit" (rows), t = "rabbit" (columns):
| "" | r | a | b | b | i | t | |
|---|---|---|---|---|---|---|---|
| "" | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| r | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
| a | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
| b | 1 | 1 | 1 | 1 | 0 | 0 | 0 |
| b | 1 | 1 | 1 | 2 | 1 | 0 | 0 |
| b | 1 | 1 | 1 | 3 | 3 | 0 | 0 |
| i | 1 | 1 | 1 | 3 | 3 | 3 | 0 |
| t | 1 | 1 | 1 | 3 | 3 | 3 | 3 |
For example, at the third b: dp[5][4] = dp[4][4] + dp[4][3] = 1 + 2 = 3.
Code
class Solution {
public int numDistinct(String s, String t) {
int m = s.length(), n = t.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
dp[i][0] = 1; // empty t is matched once
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = dp[i - 1][j]; // skip s[i-1]
if (s.charAt(i - 1) == t.charAt(j - 1)) {
dp[i][j] += dp[i - 1][j - 1]; // use s[i-1] for t[j-1]
}
}
}
return dp[m][n];
}
}Complexity
- Time: O(m · n).
- Space: O(m · n).
Space-Optimized 1-D DPOptimal
TimeO(m · n).SpaceO(n)Intuition
Row i reads only row i - 1 (same column and one to the left). Keep one array dp[j] and iterate j from right to left: then dp[j - 1] has not been touched yet in this row and still holds the previous row's value — exactly the dp[i-1][j-1] we need. The update becomes dp[j] += dp[j - 1] when the characters match.
Algorithm
dp = new int[n + 1];dp[0] = 1.- For each character
cofs: forj = ndown to1: ifc == t[j-1],dp[j] += dp[j-1]. - Return
dp[n].
Dry Run
flowchart TD
A["t = 'bag'<br/>dp = [1,0,0,0]"] -->|"s[0]='b'"| B["[1,1,0,0]"]
B -->|"s[1]='a'"| C["[1,1,1,0]"]
C -->|"s[2]='b'"| D["[1,2,1,0]"]
D -->|"s[3]='g'"| E["[1,2,1,1]"]
E -->|"s[4]='b'"| F["[1,3,1,1]"]
F -->|"s[5]='a'"| G["[1,3,4,1]"]
G -->|"s[6]='g'"| H["[1,3,4,5]"]
H --> I["answer dp[3] = 5"]Code
class Solution {
public int numDistinct(String s, String t) {
int n = t.length();
int[] dp = new int[n + 1];
dp[0] = 1; // empty t
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// right-to-left so dp[j-1] is still the previous row's value
for (int j = n; j >= 1; j--) {
if (c == t.charAt(j - 1)) {
dp[j] += dp[j - 1];
}
}
}
return dp[n];
}
}Complexity
- Time: O(m · n).
- Space: O(n) — one row over
t.
Complexity Comparison
| Approach | Time | Space |
|---|---|---|
| Recursion (Brute Force) | O(2^m) | O(m) |
| Top-Down Memoization | O(m · n) | O(m · n) |
| Bottom-Up 2-D Tabulation | O(m · n) | O(m · n) |
| Space-Optimized 1-D DP | O(m · n) | O(n) |
Common Pitfalls
- Iterating
jleft-to-right in the 1-D version —dp[j-1]would already include the current character, so onescharacter could match twotcharacters. - Base cases in the wrong order: check
j == t.length()beforei == s.length(), since matching all oftat the very end ofsis a success. - Forgetting
dp[i][0] = 1for every row (not justdp[0][0]). - Some LeetCode tests produce intermediate counts beyond
intrange while the final answer fits; Javaintwrap-around keeps the final sum correct, but switching tolongalone does not prevent the overflow in other languages. - Adding an early exit
if (m < n) return 0;is a nice optimization, but make sure it doesn't skip thet == ""case (answer1).