🧠
Top-Down DP with Memoization
💡 Memoization avoids recomputing the same subproblems by caching results, drastically improving efficiency over brute force.
Intuition
Use recursion but store results of dp[i][j] to avoid repeated work. This exploits overlapping subproblems.
Algorithm
- Initialize a memo dictionary to store results for (i, j).
- If (i, j) in memo, return stored result.
- Apply same logic as brute force but store results before returning.
- Return memo[(i, j)] as the result for current state.
💡 Memoization adds a caching layer that transforms exponential recursion into polynomial time.
Recurrence:dp[i][j] = (p[j] == '*') ? dp[i][j+1] or dp[i+1][j] : (p[j] == s[i] or p[j] == '?') and dp[i+1][j+1]
def isMatch(s: str, p: str) -> bool:
memo = {}
def dfs(i, j):
if (i, j) in memo:
return memo[(i, j)]
if i == len(s) and j == len(p):
return True
if j == len(p):
return False
if p[j] == '*':
memo[(i, j)] = dfs(i, j+1) or (i < len(s) and dfs(i+1, j))
return memo[(i, j)]
if i < len(s) and (p[j] == s[i] or p[j] == '?'):
memo[(i, j)] = dfs(i+1, j+1)
return memo[(i, j)]
memo[(i, j)] = False
return False
return dfs(0, 0)
# Example usage
if __name__ == '__main__':
print(isMatch('adceb', '*a*b')) # True
print(isMatch('aa', 'a')) # False
Line Notes
memo = {}Cache to store results of subproblems
if (i, j) in memo:Return cached result to avoid recomputation
memo[(i, j)] = dfs(i, j+1) or (i < len(s) and dfs(i+1, j))Store result for '*' wildcard matching zero or more chars
memo[(i, j)] = dfs(i+1, j+1)Store result for matching single char or '?'
import java.util.*;
public class Solution {
Map<String, Boolean> memo = new HashMap<>();
public boolean isMatch(String s, String p) {
return dfs(s, p, 0, 0);
}
private boolean dfs(String s, String p, int i, int j) {
String key = i + "," + j;
if (memo.containsKey(key)) return memo.get(key);
if (i == s.length() && j == p.length()) return true;
if (j == p.length()) return false;
boolean ans;
if (p.charAt(j) == '*') {
ans = dfs(s, p, i, j + 1) || (i < s.length() && dfs(s, p, i + 1, j));
} else if (i < s.length() && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '?')) {
ans = dfs(s, p, i + 1, j + 1);
} else {
ans = false;
}
memo.put(key, ans);
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.isMatch("adceb", "*a*b")); // true
System.out.println(sol.isMatch("aa", "a")); // false
}
}
Line Notes
Map<String, Boolean> memo = new HashMap<>();Cache for storing subproblem results
if (memo.containsKey(key)) return memo.get(key);Return cached result if available
ans = dfs(s, p, i, j + 1) || (i < s.length() && dfs(s, p, i + 1, j));Handle '*' wildcard with memoization
memo.put(key, ans);Store computed result before returning
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
class Solution {
unordered_map<string, bool> memo;
public:
bool dfs(const string& s, const string& p, int i, int j) {
string key = to_string(i) + "," + to_string(j);
if (memo.count(key)) return memo[key];
if (i == s.size() && j == p.size()) return true;
if (j == p.size()) return false;
bool ans;
if (p[j] == '*') {
ans = dfs(s, p, i, j + 1) || (i < s.size() && dfs(s, p, i + 1, j));
} else if (i < s.size() && (p[j] == s[i] || p[j] == '?')) {
ans = dfs(s, p, i + 1, j + 1);
} else {
ans = false;
}
memo[key] = ans;
return ans;
}
bool isMatch(string s, string p) {
return dfs(s, p, 0, 0);
}
};
int main() {
Solution sol;
cout << boolalpha << sol.isMatch("adceb", "*a*b") << endl; // true
cout << boolalpha << sol.isMatch("aa", "a") << endl; // false
return 0;
}
Line Notes
unordered_map<string, bool> memo;Cache to store results of subproblems
if (memo.count(key)) return memo[key];Return cached result if available
ans = dfs(s, p, i, j + 1) || (i < s.size() && dfs(s, p, i + 1, j));Handle '*' wildcard with memoization
memo[key] = ans;Store computed result before returning
function isMatch(s, p) {
const memo = new Map();
function dfs(i, j) {
const key = i + ',' + j;
if (memo.has(key)) return memo.get(key);
if (i === s.length && j === p.length) return true;
if (j === p.length) return false;
let ans;
if (p[j] === '*') {
ans = dfs(i, j + 1) || (i < s.length && dfs(i + 1, j));
} else if (i < s.length && (p[j] === s[i] || p[j] === '?')) {
ans = dfs(i + 1, j + 1);
} else {
ans = false;
}
memo.set(key, ans);
return ans;
}
return dfs(0, 0);
}
// Example usage
console.log(isMatch('adceb', '*a*b')); // true
console.log(isMatch('aa', 'a')); // false
Line Notes
const memo = new Map();Cache to store subproblem results
if (memo.has(key)) return memo.get(key);Return cached result if available
ans = dfs(i, j + 1) || (i < s.length && dfs(i + 1, j));Handle '*' wildcard with memoization
memo.set(key, ans);Store computed result before returning
TimeO(m*n)
SpaceO(m*n) due to memoization cache and recursion stack
Each state (i, j) is computed once and cached, reducing exponential calls to polynomial.
💡 For strings of length 1000, this means up to 1 million states, which is feasible.
Interview Verdict: Accepted
Memoization makes the solution efficient enough for large inputs and is a common interview expectation.