Regular Expression Matching — The Hardest 2D DP String Problem in FAANG Interviews
Advertisement
Problem Statement
Given an input string
sand a patternp, implement regular expression matching with support for'.'and'*'where:
'.'matches any single character.'*'matches zero or more of the preceding element.The matching must cover the entire input string (not partial).
Constraints:
1 <= s.length <= 201 <= p.length <= 30scontains only lowercase English letterspcontains only lowercase English letters,'.', and'*'- It is guaranteed that for each occurrence of
'*', there will be a valid preceding element to match
Example 1:
Input: s = "aa", p = "a*"
Output: true
Explanation: '*' means zero or more 'a', and "a*" matches "aa".Example 2:
Input: s = "ab", p = ".*"
Output: true
Explanation: ".*" matches any string.Example 3:
Input: s = "aab", p = "c*a*b"
Output: true
Explanation: 'c' is repeated 0 times, 'a' is repeated 2 times. "c*a*b" matches "aab".Why This Problem Matters
Regular Expression Matching (LC 10) is considered the hardest standard 2D DP string problem in the FAANG interview canon. Google, Meta, and Microsoft ask it at senior engineer level because it requires handling multiple overlapping cases for the * operator in a single recurrence — and getting the cases wrong produces plausible-looking but subtly incorrect solutions.
The core difficulty: * can match zero occurrences (skip the preceding char-star pair entirely) or one-or-more occurrences (consume a character and stay at the same pattern position). Managing both cases simultaneously in the DP recurrence requires careful state design that many candidates find non-intuitive.
Beyond the interview, this problem teaches you how regex engines actually work internally — a valuable insight for understanding NFA/DFA construction and string parsing algorithms.
The Core Insight
Define dp[i][j] = True if s[:i] matches p[:j].
Base cases:
dp[0][0] = True— empty string matches empty pattern.dp[i][0] = Falsefori > 0— non-empty string cannot match empty pattern.dp[0][j]: empty string can match patterns likea*,a*b*, etc. (star pairs that match zero occurrences). Setdp[0][j] = dp[0][j-2]ifp[j-1] == '*'.
Recurrence for dp[i][j] (i > 0, j > 0):
Case 1: p[j-1] is a regular character or .:
Match the current character and check if the prefixes match:
dp[i][j] = dp[i-1][j-1] and (p[j-1] == s[i-1] or p[j-1] == '.')Case 2: p[j-1] == '*':
The star can match zero or more of p[j-2] (the preceding character).
- Zero occurrences: skip the
x*pair entirely.dp[i][j] |= dp[i][j-2] - One or more occurrences: if
p[j-2]matchess[i-1](i.e.,p[j-2] == s[i-1]orp[j-2] == '.'), then the star "consumed" one more character ofs:dp[i][j] |= dp[i-1][j] (and p[j-2] matches s[i-1])
Full * case:
if p[j-1] == '*':
dp[i][j] = dp[i][j-2] # zero occurrences
if p[j-2] == s[i-1] or p[j-2] == '.':
dp[i][j] = dp[i][j] or dp[i-1][j] # one+ occurrencesAnswer: dp[len(s)][len(p)]
Building the DP Solution
Step 1 — Recursive (exponential):
def isMatch(s, p):
if not p: return not s
first_match = bool(s) and p[0] in {s[0], '.'}
if len(p) >= 2 and p[1] == '*':
return isMatch(s, p[2:]) or (first_match and isMatch(s[1:], p))
return first_match and isMatch(s[1:], p[1:])Step 2 — Memoized recursion:
from functools import lru_cache
def isMatch(s, p):
@lru_cache(None)
def dp(i, j):
if j == len(p): return i == len(s)
first = i < len(s) and p[j] in {s[i], '.'}
if j + 1 < len(p) and p[j + 1] == '*':
return dp(i, j + 2) or (first and dp(i + 1, j))
return first and dp(i + 1, j + 1)
return dp(0, 0)Step 3 — Bottom-up 2D tabulation: See Optimized Solution below.
Visual Dry Run
Input: s = "aab", p = "c*a*b"
| "" | c | c* | a | a* | b | |
|---|---|---|---|---|---|---|
| "" | T | F | T | F | T | F |
| a | F | F | F | T | T | F |
| a | F | F | F | F | T | F |
| b | F | F | F | F | F | T |
Key cells:
dp[0][2]: p[1]='*' → zero occurrences:dp[0][0]=T→ Tdp[0][4]: p[3]='*' → zero occurrences:dp[0][2]=T→ Tdp[1][4]: p[3]='*'. Zero:dp[1][2]=F. One+: p[2]='a'==s[0]='a', so checkdp[0][4]=T→ Tdp[2][4]: p[3]='*'. Zero:dp[2][2]=F. One+: p[2]='a'==s[1]='a', checkdp[1][4]=T→ Tdp[3][5]: p[4]='b'==s[2]='b', checkdp[2][4]=T→ T
Optimized Solution
Python
class Solution:
def isMatch(self, s: str, p: str) -> bool:
m, n = len(s), len(p)
# dp[i][j] = True if s[:i] matches p[:j]
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True
# Handle patterns that match empty string: a*, a*b*, etc.
for j in range(2, n + 1):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 2] # zero occurrences of p[j-2]
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j - 1] == '*':
# Zero occurrences: skip the 'x*' pair
dp[i][j] = dp[i][j - 2]
# One or more occurrences: p[j-2] must match s[i-1]
if p[j - 2] == s[i - 1] or p[j - 2] == '.':
dp[i][j] = dp[i][j] or dp[i - 1][j]
elif p[j - 1] == s[i - 1] or p[j - 1] == '.':
# Direct match or wildcard '.'
dp[i][j] = dp[i - 1][j - 1]
# else: characters don't match — dp[i][j] stays False
return dp[m][n]JavaScript
var isMatch = function(s, p) {
const m = s.length, n = p.length;
const dp = Array.from({length: m + 1}, () => new Array(n + 1).fill(false));
dp[0][0] = true;
// Handle patterns matching empty string
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] === s[i - 1] || p[j - 2] === '.') {
dp[i][j] = dp[i][j] || dp[i - 1][j]; // one or more
}
} else if (p[j - 1] === s[i - 1] || p[j - 1] === '.') {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return dp[m][n];
};Complexity Analysis
| Approach | Time | Space | Notes |
|---|---|---|---|
| Recursive (no memo) | O(2^(m+n)) | O(m+n) | Exponential — worst case |
| Memoized recursion | O(m * n) | O(m * n) | Correct, call stack overhead |
| 2D DP tabulation | O(m * n) | O(m * n) | Clean, debuggable |
1D rolling (tricky for * cases) | O(m * n) | O(n) | Space-optimized — handle carefully |
Common Mistakes
1. Forgetting to initialize dp[0][j] for star patterns.
Patterns like a*, a*b* can match an empty string by using zero occurrences. dp[0][j] = dp[0][j-2] when p[j-1] == '*'. Missing this makes all patterns starting with stars incorrectly fail on empty strings.
2. Confusing "zero occurrences" and "one or more" for *.
Zero occurrences: dp[i][j] = dp[i][j-2] (skip the char-star pair).
One or more: dp[i][j] |= dp[i-1][j] when p[j-2] matches s[i-1]. Both cases must be checked. Many candidates only implement one.
3. Checking p[j-2] without ensuring j >= 2.
When p[j-1] == '*' and j < 2, accessing p[j-2] is out of bounds. The constraint guarantees that * always has a preceding character, so j >= 2 is always true when p[j-1] == '*' in a valid pattern. But always verify this assumption.
4. Confusing LC 10 (regex *) with LC 44 (wildcard *).
In LC 10, * means "zero or more of the PRECEDING character." In LC 44, * means "any sequence of any characters." The recurrences are completely different. Never mix them.
5. Using s[i] and p[j] (0-indexed) in a 1-indexed DP table.
In the 1-indexed DP, s[i-1] is the i-th character of s, and p[j-1] is the j-th character of p. Off-by-one indexing is the most common source of silent bugs in this problem.
Interview Tips
- Tackle the non-star case first: "Without
*, the recurrence is simply:dp[i][j] = dp[i-1][j-1] and (s[i-1] == p[j-1] or p[j-1] == '.'). Then add*handling." - Explain the
*semantics clearly: "*means zero or more of the PRECEDING character — not the star itself. Soa*represents the pair, not justaor just*." - Walk through Example 3 aloud:
s = "aab",p = "c*a*b". Show howc*disappears (zero matches),a*matches two 'a's, and 'b' matches 'b'. - Compare to LC 44: "In Wildcard Matching (LC 44),
*can match any sequence directly. Here,*modifies the preceding character — the table structure is the same, but the transitions differ." - Mention memoized recursion as an alternative: "The recursive approach with
@lru_cacheis often easier to reason about during an interview, especially for the*cases."
Follow-up Questions
Q: How does this differ from LC 44 Wildcard Matching?
LC 44's * matches any sequence of characters directly. LC 10's * modifies the preceding character. In LC 44, the * transition is dp[i][j] = dp[i-1][j] or dp[i][j-1]. In LC 10, it is dp[i][j] = dp[i][j-2] or (match and dp[i-1][j]).
Q: What if the pattern can have consecutive stars like a**?
The problem guarantees valid patterns — consecutive stars are not valid regex. In practice, this would need preprocessing to simplify a** to a*.
Q: How do real regex engines handle this? Real engines convert the pattern to an NFA (nondeterministic finite automaton) and simulate it. The DP approach is equivalent to NFA simulation but implemented as a table rather than state traversal. Both are O(m * n) for simple patterns.
Q: Can this be solved with a 1D rolling array?
Yes, but it's tricky: dp[j-2] is used in the * case, so you need to be careful about the order of updates. Typically the 2D table is preferred in interviews for clarity.
Key Takeaways
dp[i][j]= True ifs[:i]matchesp[:j].- Three cases: regular char match (diagonal),
.match (diagonal),*(zero:j-2, one+:i-1if preceding char matches). - Initialize
dp[0][j] = dp[0][j-2]whenp[j-1] == '*'— this handles patterns likea*b*c*matching an empty string. - The
*in LC 10 modifies the PRECEDING character — fundamentally different from the*in LC 44 (wildcard matching). - This is the hardest standard 2D DP string problem in FAANG interviews. Mastering it requires internalizing all three
*cases and the empty-string initialization. - Google and Meta use this problem specifically because the star's semantics are subtle and the recurrence is non-obvious — precisely what distinguishes strong candidates.
Advertisement