Given a string s and a pattern p with . (any single char) and * (zero or more of the preceding), decide if p matches all of s. A classic 2D DP table over prefixes of both strings handles every case cleanly.
Return true if string s is fully matched by pattern p. Rules: . matches any single character; * matches zero or more copies of the character immediately before it. The entire string must be covered — not just a substring.
Concrete example: s="aab", p="c*a*b" → true. The c* matches zero cs, a* matches two as, and b matches the final b.
Another: s="aa", p="a" → false (pattern too short). s="aa", p="a*" → true.
dp[i][j] as "does s[0..i-1] match p[0..j-1]?". Because matching a prefix of s against a prefix of p only depends on smaller prefixes, we can fill the table left-to-right, top-to-bottom, and each cell is answered by at most two earlier cells. The * rule is the key branch: either skip the x* pair entirely (dp[i][j-2]), or — if the preceding char matches — extend by one more character ( dp[i-1][j]).dp[0][0] = true (empty matches empty). For column 0 with j > 0: only patterns like a*, a*b* can match the empty string — set dp[0][j] = dp[0][j-2] whenever p[j-1] === '*'.*. Two sub-cases:x* pair → dp[i][j] |= dp[i][j-2].p[j-2]) is . or equals s[i-1], consume one more char of s → dp[i][j] |= dp[i-1][j].. or exact match. Direct consume: dp[i][j] = dp[i-1][j-1].dp[i][j] = false.*"one or more" branch uses dp[i-1][j] (not dp[i-1][j-1] and not dp[i-1][j-2]). Staying at the same j is what lets *consume arbitrarily many characters — the pattern "stays" while s advances. Moving to j-1 or j-2 is the most common off-by-one mistake on this problem.Space reduction: because each row only reads from the row above, you can keep just two rows at a time — prev and curr — cutting space from O(m·n) to O(n).
* matches any sequence directly), Edit Distance (LC 72), Distinct Subsequences (LC 115), and Interleaving String (LC 97). Whenever you must reconcile two strings character-by-character with optional skips, reach for this shape.(m+1)×(n+1) DP table. dp[i][j] = does s[0..2] match p[0..4]? All cells start false.1function isMatch(s: string, p: string): boolean {2 const m = s.length;3 const n = p.length;45 // dp[i][j] = true if s[0..i-1] matches p[0..j-1]6▶ const dp: boolean[][] = Array.from({ length: m + 1 }, () =>7▶ new Array(n + 1).fill(false),8▶ );9 dp[0][0] = true; // empty string matches empty pattern1011 // empty string can match "a*", "a*b*", etc.12 for (let j = 2; j <= n; j++) {13 if (p[j - 1] === '*') dp[0][j] = dp[0][j - 2];14 }1516 for (let i = 1; i <= m; i++) {17 for (let j = 1; j <= n; j++) {18 const sc = s[i - 1]; // current char of s19 const pc = p[j - 1]; // current char of p2021 if (pc === '*') {22 // Case 1: zero occurrences of p[j-2] — skip the "x*" pair23 dp[i][j] = dp[i][j - 2];2425 // Case 2: one-or-more occurrences — p[j-2] must match sc26 if (p[j - 2] === '.' || p[j - 2] === sc) {27 dp[i][j] = dp[i][j] || dp[i - 1][j];28 }29 } else if (pc === '.' || pc === sc) {30 // Direct match: '.' or exact char31 dp[i][j] = dp[i - 1][j - 1];32 }33 }34 }3536 return dp[m][n];37}
function isMatch(s: string, p: string): boolean {
const m = s.length;
const n = p.length;
// dp[i][j] = true if s[0..i-1] matches p[0..j-1]
const dp: boolean[][] = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(false),
);
dp[0][0] = true; // empty string matches empty pattern
// empty string can match "a*", "a*b*", etc.
for (let j = 2; j <= n; j++) {
if (p[j - 1] === '*') dp[0][j] = dp[0][j - 2];
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const sc = s[i - 1]; // current char of s
const pc = p[j - 1]; // current char of p
if (pc === '*') {
// Case 1: zero occurrences of p[j-2] — skip the "x*" pair
dp[i][j] = dp[i][j - 2];
// Case 2: one-or-more occurrences — p[j-2] must match sc
if (p[j - 2] === '.' || p[j - 2] === sc) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
} else if (pc === '.' || pc === sc) {
// Direct match: '.' or exact char
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return dp[m][n];
}dp is an (m+1) × (n+1) boolean grid where index 0 represents the empty prefix. All cells start false.a*, a*b* can match the empty string by using zero of each quantified char. Only even-indexed columns (those ending in *) can be true here.(i,j) pair, look at the current pattern char p[j-1]. The *branch checks both the "zero occurrences" case (dp[i][j-2]) and the "one-or-more" case (dp[i-1][j], provided the preceding pattern char matches s[i-1]). The direct-match branch just propagates the diagonal.dp[m][n] asks: does all of s (length m) match all of p (length n)?prev and curr rows, cutting space from O(m·n) to O(n).s[0] — start multiple DP paths from each index of s.* matches any sequence by itself (not zero-or-more of the preceding char), so the DP has only one branch for *: dp[i][j] = dp[i-1][j] || dp[i][j-1].match(i, j) with a 2D memo array. Same recurrence, same complexity — but top-down with early termination might be slightly faster in practice for sparse matches.s with pattern a*b* (should be true); pattern longer than string; consecutive * (invalid per constraints but worth naming).function isMatch(s: string, p: string): boolean {
const m = s.length;
const n = p.length;
// dp[i][j] = true if s[0..i-1] matches p[0..j-1]
const dp: boolean[][] = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(false),
);
dp[0][0] = true; // empty string matches empty pattern
// empty string can match "a*", "a*b*", etc.
for (let j = 2; j <= n; j++) {
if (p[j - 1] === '*') dp[0][j] = dp[0][j - 2];
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const sc = s[i - 1]; // current char of s
const pc = p[j - 1]; // current char of p
if (pc === '*') {
// Case 1: zero occurrences of p[j-2] — skip the "x*" pair
dp[i][j] = dp[i][j - 2];
// Case 2: one-or-more occurrences — p[j-2] must match sc
if (p[j - 2] === '.' || p[j - 2] === sc) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
} else if (pc === '.' || pc === sc) {
// Direct match: '.' or exact char
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return dp[m][n];
}Express the recurrence directly as match(i, j) and cache every (i, j) result so each state is solved once — same asymptotics as the table, but only the reachable states get computed.
function isMatch(s: string, p: string): boolean {
const m = s.length;
const n = p.length;
// memo[i][j] caches the answer to match(i, j); null = not yet computed.
const memo: (boolean | null)[][] = Array.from({ length: m + 1 }, () =>
new Array<boolean | null>(n + 1).fill(null),
);
// Does s[i..] match p[j..]?
function match(i: number, j: number): boolean {
const cached = memo[i][j];
if (cached !== null) return cached;
let ans: boolean;
if (j === n) {
// Pattern exhausted: only a match if s is also exhausted.
ans = i === m;
} else {
// Does the current single char of s match the current pattern char?
const firstMatch = i < m && (p[j] === '.' || p[j] === s[i]);
if (j + 1 < n && p[j + 1] === '*') {
// 'x*' => either skip the pair (zero occurrences), or, if the
// current char matches, consume one char of s and stay on the pair.
ans = match(i, j + 2) || (firstMatch && match(i + 1, j));
} else {
// Ordinary char or '.': both sides must advance by one.
ans = firstMatch && match(i + 1, j + 1);
}
}
memo[i][j] = ans;
return ans;
}
return match(0, 0);
}s[i..] vs p[j..]) rather than the table's prefix indexing, but the two * branches are identical: match(i, j + 2) is "zero occurrences" and match(i + 1, j)is "one or more." The cache turns the otherwise-exponential tree into O(m·n) distinct states.The exact same branching with the cache removed — correct, but the overlapping * sub-calls are recomputed exponentially. This is the baseline that motivates memoization and the DP table.
function isMatch(s: string, p: string): boolean {
const m = s.length;
const n = p.length;
// Does s[i..] match p[j..]? No caching — every call recomputes from scratch.
function match(i: number, j: number): boolean {
if (j === n) {
// Pattern exhausted: match only if s is exhausted too.
return i === m;
}
// Does the current single char of s match the current pattern char?
const firstMatch = i < m && (p[j] === '.' || p[j] === s[i]);
if (j + 1 < n && p[j + 1] === '*') {
// 'x*' => skip the pair (zero occurrences), OR consume one matching
// char of s and stay on the pair (one or more occurrences).
return match(i, j + 2) || (firstMatch && match(i + 1, j));
}
// Ordinary char or '.': advance both sides by one.
return firstMatch && match(i + 1, j + 1);
}
return match(0, 0);
}memo and the branching is unchanged — proof that the only thing memoization adds is reuse. On inputs with many * quantifiers (e.g. s="aaaaaaaaaa", p="a*a*a*a*b") this blows up to exponential time; add the cache and it collapses back to O(m·n).| "does the entire string match a regex-like pattern" | 2D DP over s and p prefixes |
| "pattern has . and * wildcards, full-string match" | dp[i][j] = does s[0..i-1] match p[0..j-1] |
| "zero or more of preceding char" (Kleene star) | two branches: dp[i][j-2] (zero) or dp[i-1][j] (more) |
| "related: Wildcard Matching, Edit Distance" | same two-string prefix DP shape |
const m = s.length, n = p.length;
const dp: boolean[][] = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(false));
dp[0][0] = true;
for (let j = 2; j <= n; j++)
if (p[j-1] === '*') dp[0][j] = dp[0][j-2];
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (p[j-1] === '*') {
dp[i][j] = dp[i][j-2]; // zero occurrences
if (p[j-2] === '.' || p[j-2] === s[i-1])
dp[i][j] = dp[i][j] || dp[i-1][j]; // one+ occurrences
} else if (p[j-1] === '.' || p[j-1] === s[i-1]) {
dp[i][j] = dp[i-1][j-1]; // direct match
}
}
}
return dp[m][n];true if s[0..i-1] (first i chars) is fully matched by p[0..j-1] (first j chars).isMatch(s, p)?p[j-1] === '*', which cell provides the "zero occurrences" case?p[j-1] === '*' and the preceding char matches s[i-1], which cell provides the "one or more" case?s="aab", p="c*a*b". What does isMatch return?dp[0][j] be true for some j > 0?s="ab", p=".*". Result?