Count how many distinct ways you can delete characters from s to produce t. A 2D DP table where dp[i][j] counts ways s[i:] can form t[j:] — at each matching pair you either skip s[i] or use both characters.
Given strings s and t, return the number of distinct ways to delete characters from s so that the remaining characters equal t (in order). Each position in s can be used at most once.
Worked example: s = "rabbbit", t = "rabbit". The extra 'b' in scan be the "dropped" one in three distinct positions, so the answer is 3.
Another: s = "babgbag", t = "bag" → 5. There are five distinct index-triples in sthat spell "bag".
dp[i][j] = number of ways s[i:] can form t[j:]. When s[i] === t[j] you have two independent choices: skip s[i] and stay on t[j](the "skip-s" branch, worth dp[i+1][j]ways), or match them both and advance (the "take-both" branch, worth dp[i+1][j+1] ways). Add the two. When characters differ you can only skip s[i]. The answer is dp[0][0].dp[i][j] = ways s[i..m-1] can form t[j..n-1]. Suffixes, not prefixes — this keeps the recurrence additive.dp[i][n] = 1 for all i: an empty target is trivially matched. dp[m][j] = 0 for j < n: no source left, target still non-empty = impossible.s[i] and try to form the same t[j:] from the rest: dp[i][j] += dp[i+1][j].s[i] === t[j], also add dp[i+1][j+1]: consume both characters and count the ways the remaining suffixes align.i from m-1 down to 0, inner loop j from n-1 down to 0. Each cell needs dp[i+1][...], which is already filled.dp[0][0].dp[i][j]as "prefixes" (s[0..i] forms t[0..j]) and recur backward — that's equivalent, but the suffix formulation makes the take-both branch feel more natural: dp[i+1][j+1] rather than dp[i-1][j-1]. Either direction is valid; be consistent and never mix the two.Because row i only ever reads row i+1, you can keep just two 1-D arrays (or one array iterated right-to-left) and cut space from O(m·n) to O(n). Iterate jfrom high to low so you don't overwrite a value you still need.
dp[i][n] = 1 for all i (empty t matched 1 way), and dp[m][j] = 0 for j < n (empty s, non-empty t = 0 ways).1▶function numDistinct(s: string, t: string): number {2▶ const m = s.length, n = t.length;3 // dp[i][j] = number of ways s[i..m-1] can form t[j..n-1]4▶ const dp: number[][] = Array.from({ length: m + 1 }, () =>5▶ new Array(n + 1).fill(0)6▶ );7 // Base case: empty t is matched by any suffix of s exactly 1 way8▶ for (let i = 0; i <= m; i++) dp[i][n] = 1;910 // Fill bottom-up: iterate i right-to-left, j right-to-left11 for (let i = m - 1; i >= 0; i--) {12 for (let j = n - 1; j >= 0; j--) {13 // skip s[i]: always allowed — move to dp[i+1][j]14 dp[i][j] = dp[i + 1][j];15 // take-both: only when characters match16 if (s[i] === t[j]) dp[i][j] += dp[i + 1][j + 1];17 }18 }19 return dp[0][0];20}
function numDistinct(s: string, t: string): number {
const m = s.length, n = t.length;
// dp[i][j] = number of ways s[i..m-1] can form t[j..n-1]
const dp: number[][] = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(0)
);
// Base case: empty t is matched by any suffix of s exactly 1 way
for (let i = 0; i <= m; i++) dp[i][n] = 1;
// Fill bottom-up: iterate i right-to-left, j right-to-left
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
// skip s[i]: always allowed — move to dp[i+1][j]
dp[i][j] = dp[i + 1][j];
// take-both: only when characters match
if (s[i] === t[j]) dp[i][j] += dp[i + 1][j + 1];
}
}
return dp[0][0];
}(m+1) × (n+1)grid initialised to zero. The extra row and column hold the base cases; their indices correspond to "nothing left of s" and "nothing left of t" respectively.dp[i][n] = 1 for every isays: "once t is exhausted, there is exactly one way to match — stop deleting." The row dp[m][j] for j < n remains zero: s is exhausted but t is not; impossible.i from m-1 down to 0 (right-to-left over s) and j from n-1 down to 0 (right-to-left over t). Each cell needs dp[i+1][...], which is already computed.dp[i][j] = dp[i+1][j] counts all the ways we can form t[j:] without using s[i]at all. This is always valid — we're just ignoring one character of s.s[i] === t[j] do we also add dp[i+1][j+1]: we consume s[i] as the match for t[j] and recurse on the remaining suffixes. The two branches are independent so we add (not take-max).j right-to-left to avoid overwriting the dp[i+1][j+1] value before it's read.j from n-1 down to 0 so you read dp[j+1] before overwriting dp[j].dp[0][0], choosing the skip-s path or take-both path based on which branch contributed.2ⁿ subsequences of s and count those equal to t. DP memoises the repeated suffix sub-problems that make recursion exponential.function numDistinct(s: string, t: string): number {
const m = s.length, n = t.length;
// dp[i][j] = number of ways s[i..m-1] can form t[j..n-1]
const dp: number[][] = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(0)
);
// Base case: empty t is matched by any suffix of s exactly 1 way
for (let i = 0; i <= m; i++) dp[i][n] = 1;
// Fill bottom-up: iterate i right-to-left, j right-to-left
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
// skip s[i]: always allowed — move to dp[i+1][j]
dp[i][j] = dp[i + 1][j];
// take-both: only when characters match
if (s[i] === t[j]) dp[i][j] += dp[i + 1][j + 1];
}
}
return dp[0][0];
}j right-to-left to avoid overwriting the dp[i+1][j+1] value before it's read.Recurse on the suffixes (i, j)and cache each result — the most direct translation of the "skip or take-both" recurrence.
function numDistinct(s: string, t: string): number {
const m = s.length, n = t.length;
// memo[i][j] = ways s[i:] can form t[j:]; -1 = not yet computed
const memo: number[][] = Array.from({ length: m + 1 }, () =>
new Array<number>(n + 1).fill(-1)
);
function solve(i: number, j: number): number {
if (j === n) return 1; // empty target: one way (delete the rest)
if (i === m) return 0; // source exhausted, target left: impossible
if (memo[i][j] !== -1) return memo[i][j];
let ways = solve(i + 1, j); // skip s[i] — always allowed
if (s[i] === t[j]) {
ways += solve(i + 1, j + 1); // take both — only on a match
}
memo[i][j] = ways;
return ways;
}
return solve(0, 0);
}O(m·n) states as the bottom-up table, just filled lazily. Recursion depth is O(m + n); for very long s the explicit table avoids stack-overflow risk.Collapse the table to a single row over t, updating it right-to-left as each character of s is consumed.
function numDistinct(s: string, t: string): number {
const m = s.length, n = t.length;
// dp[j] = ways the processed prefix of s can form t[0:j]
const dp: number[] = new Array<number>(n + 1).fill(0);
dp[0] = 1; // empty target is matched exactly one way
for (let i = 0; i < m; i++) {
// iterate j right-to-left so dp[j-1] still holds the PREVIOUS
// row's value (s[i] used at most once per target position)
for (let j = n; j >= 1; j--) {
if (s[i] === t[j - 1]) {
dp[j] += dp[j - 1];
}
}
}
return dp[n];
}dp[j - 1] reflects the row before s[i] was added, so each source character contributes to a given target position at most once. Left-to-right would let one s[i] be reused and overcount.Recursively decide to keep or drop each character of s, and count the resulting subsequences that exactly equal t— the literal definition of the answer.
function numDistinct(s: string, t: string): number {
const m = s.length;
let count = 0;
// At index i of s, having built 'built' so far, branch on keep vs skip.
function recurse(i: number, built: string): void {
if (built.length > t.length) return; // can never shrink back
if (i === m) {
if (built === t) count++; // a subsequence that matches t
return;
}
recurse(i + 1, built + s[i]); // keep s[i]
recurse(i + 1, built); // drop s[i]
}
recurse(0, '');
return count;
}2ⁿ subsequences of s and comparing each to t costs O(m), so this blows up exponentially. The DP memoises the overlapping (i, j) suffix sub-problems this recursion revisits, collapsing it to O(m·n).| "how many distinct ways" + subsequence | 2D suffix DP, add two branches |
| count subsequence occurrences of t in s | dp[i][j] = ways s[i:] forms t[j:] |
| "delete characters from s to get t" | dp[i+1][j] + (match ? dp[i+1][j+1] : 0) |
| answer too large for brute force | O(m·n) DP memoises suffix sub-problems |
function numDistinct(s: string, t: string): number {
const m = s.length, n = t.length;
const dp: number[][] = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(0)
);
for (let i = 0; i <= m; i++) dp[i][n] = 1; // base: empty t
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
dp[i][j] = dp[i + 1][j]; // skip s[i]
if (s[i] === t[j]) dp[i][j] += dp[i + 1][j + 1]; // take both
}
}
return dp[0][0];
}s[i..m-1] can form t[j..n-1] as a subsequence.dp[i][n] (the rightmost column) for every valid i?s = "rabbbit", t = "rabbit", what does the algorithm return?s[i] !== t[j], what is dp[i][j]?j-loop run?