💡 Memoization avoids recomputing the same subproblems by caching results, drastically improving efficiency over brute force.
Intuition
Store results of recursive calls in a cache keyed by indices to reuse them instead of recomputing.
Algorithm
- Create a 2D cache initialized with -1 to indicate uncomputed states.
- Modify the recursive function to check cache before computing.
- If cached, return stored value; else compute and store in cache.
- Return the cached result for each call.
💡 Memoization transforms exponential recursion into polynomial time by avoiding duplicate work.
Recurrence:dp[i][j] = dp[i+1][j] + (s[i] == t[j] ? dp[i+1][j+1] : 0)
def numDistinct(s: str, t: str) -> int:
from functools import lru_cache
@lru_cache(None)
def dfs(i, j):
if j == len(t):
return 1
if i == len(s):
return 0
res = dfs(i+1, j)
if s[i] == t[j]:
res += dfs(i+1, j+1)
return res
return dfs(0, 0)
# Example usage
if __name__ == '__main__':
print(numDistinct('rabbbit', 'rabbit')) # Output: 3
Line Notes
@lru_cache(None)Caches results of dfs calls to avoid recomputation
if j == len(t):Base case: target matched
if i == len(s):Base case: source exhausted
res = dfs(i+1, j)Count subsequences skipping current s character
if s[i] == t[j]:Count subsequences including current character if match
import java.util.*;
public class Solution {
private Integer[][] memo;
public int numDistinct(String s, String t) {
memo = new Integer[s.length()][t.length()];
return dfs(s, t, 0, 0);
}
private int dfs(String s, String t, 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 = dfs(s, t, i + 1, j);
if (s.charAt(i) == t.charAt(j)) {
res += dfs(s, t, i + 1, j + 1);
}
memo[i][j] = res;
return res;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.numDistinct("rabbbit", "rabbit")); // 3
}
}
Line Notes
private Integer[][] memo;Cache to store results of subproblems
if (memo[i][j] != null) return memo[i][j];Return cached result if available
int res = dfs(s, t, i + 1, j);Count subsequences skipping current s character
if (s.charAt(i) == t.charAt(j))Count subsequences including current character if match
memo[i][j] = res;Store computed result in cache
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Solution {
vector<vector<int>> memo;
public:
int dfs(const string& s, const string& t, int i, int j) {
if (j == t.size()) return 1;
if (i == s.size()) return 0;
if (memo[i][j] != -1) return memo[i][j];
int res = dfs(s, t, i + 1, j);
if (s[i] == t[j]) {
res += dfs(s, t, i + 1, j + 1);
}
memo[i][j] = res;
return res;
}
int numDistinct(string s, string t) {
memo.assign(s.size(), vector<int>(t.size(), -1));
return dfs(s, t, 0, 0);
}
};
int main() {
Solution sol;
cout << sol.numDistinct("rabbbit", "rabbit") << endl; // 3
return 0;
}
Line Notes
vector<vector<int>> memo;2D cache for memoization
if (memo[i][j] != -1) return memo[i][j];Return cached result if computed
int res = dfs(s, t, i + 1, j);Count subsequences skipping current s character
if (s[i] == t[j])Count subsequences including current character if match
memo[i][j] = res;Cache the computed result
function numDistinct(s, t) {
const memo = Array.from({ length: s.length }, () => Array(t.length).fill(-1));
function dfs(i, j) {
if (j === t.length) return 1;
if (i === s.length) return 0;
if (memo[i][j] !== -1) return memo[i][j];
let res = dfs(i + 1, j);
if (s[i] === t[j]) {
res += dfs(i + 1, j + 1);
}
memo[i][j] = res;
return res;
}
return dfs(0, 0);
}
console.log(numDistinct('rabbbit', 'rabbit')); // 3
Line Notes
const memo = Array.from({ length: s.length }, () => Array(t.length).fill(-1));Initialize 2D cache with -1
if (memo[i][j] !== -1) return memo[i][j];Return cached result if available
let res = dfs(i + 1, j);Count subsequences skipping current s character
if (s[i] === t[j])Count subsequences including current character if match
memo[i][j] = res;Store computed result in cache
TimeO(n*m) where n = length of s, m = length of t
SpaceO(n*m) for memoization cache and recursion stack
Each subproblem (i,j) computed once and stored, reducing exponential calls to polynomial.
💡 For n=1000 and m=1000, this means about 1 million computations, which is feasible.
Interview Verdict: Accepted
This approach is efficient enough for large inputs and is a common interview solution.