115. Distinct Subsequences
At a Glance
- Topic: dp-2d
- Pattern: Dynamic Programming — two-sequence (count ways to match subsequence)
- Difficulty: Hard
- Companies: Google, Amazon, Meta, Uber, Airbnb
- Frequency: Medium
- LeetCode: 115
Problem (one-liner)
Given strings source and target, return the number of distinct non-empty subsequences of source that equal target (modulo large prime 1e9+7 on LeetCode).
Recognition Cues
- Count subsequences equal to
target— not yes/no - Two strings → 2D DP; recurrence resembles “delete from source” or match progression through
target - Overlapping structure → table, not raw recursion
Diagram
At-a-glance flow (replace with problem-specific Mermaid as you refine this note). camelCase node IDs; no spaces in IDs.
Loading diagram…
Approaches
- Backtrack enumerate all subsequences —
O(2^len(source))— impossible. - DP
ways[sourceIndex][targetIndex]— number of ways using prefix ofsourceto form prefix oftarget—O(|source| * |target|)time,O(|target|)space with rolling row — optimal.
Optimal Solution
Go
func numDistinct(source string, target string) int {
const modulus = 1_000_000_007
sourceLen := len(source)
targetLen := len(target)
// ways[j] = ways to form target[0:j) using processed prefix of source
ways := make([]int, targetLen+1)
ways[0] = 1
for sourceIndex := 0; sourceIndex < sourceLen; sourceIndex++ {
// iterate target backwards so each char of source used at most once per path
for targetIndex := targetLen; targetIndex >= 1; targetIndex-- {
if source[sourceIndex] == target[targetIndex-1] {
ways[targetIndex] = (ways[targetIndex] + ways[targetIndex-1]) % modulus
}
}
}
return ways[targetLen]
}JavaScript
function numDistinct(source, target) {
const modulus = 1_000_000_007;
const sourceLen = source.length;
const targetLen = target.length;
const ways = new Array(targetLen + 1).fill(0);
ways[0] = 1;
for (let sourceIndex = 0; sourceIndex < sourceLen; sourceIndex += 1) {
for (let targetIndex = targetLen; targetIndex >= 1; targetIndex -= 1) {
if (source[sourceIndex] === target[targetIndex - 1]) {
ways[targetIndex] = (ways[targetIndex] + ways[targetIndex - 1]) % modulus;
}
}
}
return ways[targetLen];
}Walkthrough
source = "rabbbit", target = "rabbit". When source char matches target tail, add ways that matched one shorter prefix.
Invariant: After processing source[0:sourceIndex+1), ways[j] counts distinct alignments of target[0:j).
| step | char | ways vector (prefixes of target) |
|---|---|---|
| init | — | [1,0,0,0,0,0] |
| after… | match r | increment paths reaching first char |
Edge Cases
targetempty — usually1(empty subsequence) per problem definition; LeetCode uses non-emptytargetin constraintstargetlonger thansource—0- All characters identical — combinatorial blow-up handled by DP count
Pitfalls
- Forward inner loop over
target— double-counts using same source char multiple times in one path; reverse inner loop fixes one-pass row update - Integer overflow — apply modulus each addition
- Confusing subsequence vs substring
Similar Problems
- 1143. Longest Common Subsequence — 2D alignment family (Medium)
- 72. Edit Distance — edit cost variant (Hard)
- 10. Regular Expression Matching — pattern matching DP (Hard)
Variants
- No modulus / return bigint — same recurrence, wider integer type
- Print all distinct subsequences — backtrack with pruning (exponential output)
- Multiple targets — tensor DP or automaton merge
Mind-Map Tags
#two-string-dp #subsequence-count #rolling-array #modulo #reverse-inner-loop
Last updated on
Spotted something unclear or wrong on this page?