Distinct Subsequences
Table of Contents + β
Distinct Subsequences is a counting problem with a twist. You are not matching one string to another. You are counting how many ways one string hides inside another. The interviewer wants to see if you can build a clean two-dimensional table that counts those ways without listing them. That counting skill is the real test here.
π― The Problem
You get two strings, s and t. Count how many distinct subsequences of s are exactly equal to t.
The rules:
- A subsequence of
sis what is left after you delete some letters froms. - Deleting must keep the rest of the letters in order.
- You only choose which letters of
sto keep. You never reorder them. - Count the distinct ways to match
tinsides.
For s = "rabbbit" and t = "rabbit", the string s has three b letters in a row. To form t you keep one of those b letters and drop the others. There are three ways to choose which b to keep. So the answer is 3.
Input: s = "rabbbit", t = "rabbit"Output: 3
Explanation: pick r a b b i t from "rabbbit" in three ways.Keep two of the three b's, in these column choices: rabb_it (drop the 3rd b) rab_bit (drop the 2nd b) ra_bbit (drop the 1st b)Here is the core choice at each letter of s. If the current letter of s matches the current letter of t, you may use it or skip it. If it does not match, you must skip it.
π’ Approach 1: Try Every Choice (Brute Force)
The idea in one line: walk two pointers and add the counts from skip and use at every match.
The idea:
- Pointer
iis the position ins. Pointerjis the position int. - At each step, look at
s[i]andt[j].
How it works:
- On a match, use the letter and move both pointers, or skip it and move only
i. Add both counts. - On a mismatch, you must skip the letter of
sand move onlyi. - If
jreaches the end oft, that is one full match, so count one. - If
ireaches the end ofsbuttis not done, count zero.
Why it is weak:
- Each matching letter can split into two paths.
- The number of paths grows close to
2to the power of the length ofs. - Far too slow for long strings.
Here is the plain recursion code:
def num_distinct(s, t): def dfs(i, j): if j == len(t): return 1 if i == len(s): return 0 count = dfs(i + 1, j) if s[i] == t[j]: count += dfs(i + 1, j + 1) return count
return dfs(0, 0)π§ Approach 2: Memoization (Better)
The idea in one line: cache the count for each (i, j) pair so paths share work.
The idea:
- The same
(i, j)pair gets reached by many paths. - But the count from a given
(i, j)never changes.
How it works:
- Store the count for each
(i, j)pair the first time you compute it. - Next time you reach the same pair, read the saved count.
Why it is fast:
- There are only about
len(s) * len(t)pairs. - The work is bounded by the size of that grid, not the number of paths. This is memoization.
Here is the memoized recursion:
from functools import lru_cache
def num_distinct(s, t): @lru_cache(None) def dfs(i, j): if j == len(t): return 1 if i == len(s): return 0 return dfs(i + 1, j) + (dfs(i + 1, j + 1) if s[i] == t[j] else 0)
return dfs(0, 0)π Approach 3: 2D Counting Table (Best)
The idea in one line: fill a grid directly where each cell counts the ways for two prefixes.
The idea:
- Make a table
dpwheredp[i][j]is the ways the firstiletters ofsform the firstjletters oft. - Set the first column to one. There is exactly one way to form an empty
t: delete every letter ofs. - The rest of the first row is zero. A non-empty
tcannot be formed from an emptys.
How it works:
- You can always skip the current letter of
s, which givesdp[i-1][j]. - If the current letters match, that is
s[i-1] == t[j-1], you may also use the letter, which addsdp[i-1][j-1]. So:
if s[i-1] == t[j-1]: dp[i][j] = dp[i-1][j] + dp[i-1][j-1]else: dp[i][j] = dp[i-1][j]Why it is best:
- Each cell is filled once, so the time is O(n times m).
- No recursion and no call stack.
- The answer sits in
dp[len(s)][len(t)], which counts all the ways all ofscan form all oft.
Steps to Solve
- Make a
dptable of size(len(s)+1)by(len(t)+1), all zeros. - Set
dp[i][0] = 1for everyi. One way to form an emptytis to delete everything. - For each cell, start with
dp[i-1][j], which means skip the current letter ofs. - If
s[i-1]equalst[j-1], also adddp[i-1][j-1], which means use the current letter. - Return the bottom-right cell
dp[len(s)][len(t)].
This Python version builds a list of lists for the counting grid.
def num_distinct(s, t): n, m = len(s), len(t) dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n + 1): dp[i][0] = 1 # one way to form empty t: delete all of s
for i in range(1, n + 1): for j in range(1, m + 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] # also use s[i-1]
return dp[n][m]
print(num_distinct("rabbbit", "rabbit"))The output of the above code will be:
3Let us walk through the Python version line by line. The counting rule is the key.
n, m = len(s), len(t)dp = [[0] * (m + 1) for _ in range(n + 1)]We read both lengths. Then we build a grid with one extra row and one extra column. The extra slots stand for empty prefixes. Here dp[i][j] will count the ways the first i letters of s form the first j letters of t.
for i in range(n + 1): dp[i][0] = 1This sets the whole first column to one. An empty t can always be formed in exactly one way. Delete every letter of s. So no matter how much of s we look at, there is one way to match nothing.
for i in range(1, n + 1): for j in range(1, m + 1): dp[i][j] = dp[i - 1][j]We fill the rest cell by cell. We always start with dp[i-1][j]. This is the count where we skip the current letter of s. We pretend that letter is not there and reuse the answer without it.
if s[i - 1] == t[j - 1]: dp[i][j] += dp[i - 1][j - 1]If the current letters match, we have a second option. We can use this letter of s to cover this letter of t. That count comes from dp[i-1][j-1], the answer before both letters. We add it to the skip count. So a matching letter gives both choices summed together.
return dp[n][m]The bottom-right cell holds the full answer. It counts every distinct way all of s can form all of t.
Here is the filled grid for s = "rabbbit" and t = "rabbit". The three b letters cause the count to grow to three. The bottom-right cell is the answer.
β±οΈ Time and Space Complexity
Recursion can split at each matching letter, so it is O(2^n) time. Memoization and the 2D table both visit each (i, j) cell once. There are (n+1) * (m+1) cells. So both are O(n * m) time. The table uses O(n * m) space, but you can shrink it to one row if you reuse it carefully. Here n is the length of s and m is the length of t.
| Approach | Time Complexity | Space Complexity |
|---|---|---|
| Recursion (try every choice) | O(2^n) | O(n) |
| Memoization over (i, j) | O(n * m) | O(n * m) |
| 2D counting tabulation | O(n * m) | O(n * m) |
Tip
The big idea is βskip or useβ. You can always skip a letter of s. You can only use it when it matches the needed letter of t. Adding both counts is what makes this a counting problem, not a yes or no problem.
π§© Key Takeaways
- β
dp[i][j]counts the ways the firstiofsform the firstjoft. - β
Set the first column to one. There is one way to form an empty
t: delete all ofs. - β
You can always skip the current letter of
s, givingdp[i-1][j]. - β
When letters match, also add
dp[i-1][j-1]for using the letter. - β
The answer sits in the bottom-right cell
dp[len(s)][len(t)].
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What does the Distinct Subsequences problem count?
Why: It counts how many distinct ways t appears as a subsequence inside s.
- 2
Why is the first column of the dp table set to one?
Why: An empty t is formed in exactly one way, by deleting all of s, so dp[i][0] = 1.
- 3
When s[i-1] equals t[j-1], what is dp[i][j]?
Why: On a match you add the skip count dp[i-1][j] and the use count dp[i-1][j-1].
- 4
Where is the final answer in the table?
Why: The bottom-right cell counts all the ways the full s forms the full t.