Given an input string s and a pattern p, implement regular expression matching with support for '.' and '*' where:
'.' Matches any single character.
'*' Matches zero or more of the preceding element.
Return a boolean indicating whether the matching covers the entire input string (not partial).
Example 1:
Input: s = "aa", p = "a"
Output: false
Explanation: "a" does not match the entire string "aa".
Example 2:
Input: s = "aa", p = "a*"
Output: true
Explanation: '*' means zero or more of the preceding element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".
Example 3:
Input: s = "ab", p = ".*"
Output: true
Explanation: ".*" means "zero or more (*) of any character (.)".
Constraints:
1 <= s.length <= 20
1 <= p.length <= 20
s contains only lowercase English letters.
p contains only lowercase English letters, '.', and '*'.
It is guaranteed for each appearance of the character '*', there will be a previous valid character to match.
Given a string s and a pattern p, implement regular expression matching with two special characters:
.
Matches any single character
*
Matches zero or more of the preceding element
The matching must cover the entire input string, not just a substring.
Examples
s = "aa"p = "a"false
"a" only matches one character, not two.
s = "aa"p = "a*"true
"a*" means zero or more 'a's — matches "aa".
s = "ab"p = ".*"true
".*" means zero or more of any character — matches everything.
Step 02
Brute Force — Recursive Approach
The natural approach is recursion: compare characters from the start, and when we encounter *, branch into two choices — use it or skip it.
Recursive Logic
Base case: pattern is empty
Return true only if string is also empty
Exact match or '.' at p[0]
If s[0] matches p[0], recurse on the rest of both strings
Star at p[1]: two branches
Skip: use zero occurrences (advance pattern by 2) Consume: if s[0] matches p[0], advance string by 1 (keep pattern)
Recursion Tree Blowup
For s="aab", p="c*a*b", the recursion branches at every *:
match("aab","c*a*b")
c* skip ↙↘ c* use (c!=a, fail)
match("aab","a*b")X
a* skip ↙↘ a* use (a=a)
match("aab","b")match("ab","a*b")
a* skip ↙↘ a* use
match("ab","b")match("b","a*b")
a* skip ↙↘ a* use (a!=b)
match("b","b") = TX
Many subproblems are solved repeatedly. With longer inputs, the tree grows exponentially — O(2^(m+n)) in the worst case.
💡
Overlapping subproblems! The recursive solution re-computes the same (i, j) pairs many times. This is the classic signal that dynamic programming will help — we can memoize results in a 2D table and solve each subproblem exactly once.
Step 03
The Core Insight — Dynamic Programming
Define dp[i][j] = "does s[0..i-1] match p[0..j-1]?" We build a 2D boolean table bottom-up, where each cell depends on previously computed cells.
The Three Transition Rules
Exact / Dotp[j-1] == s[i-1] or p[j-1] == '.'
dp[i][j] = dp[i-1][j-1] — diagonal inheritance. Both characters consumed, result depends on whether the prefixes matched.
Star Zerop[j-1] == '*' (skip x*)
dp[i][j] = dp[i][j-2] — skip left by 2. Treat the "x*" as matching zero characters. Erase the pair entirely.
Star 1+p[j-1] == '*' and p[j-2] matches s[i-1]
dp[i][j] |= dp[i-1][j] — inherit from above. The "x*" consumes one character from s. The star stays in the pattern (it can match more).
Cell Dependencies Visualized
Each cell dp[i][j] can depend on up to three other cells, depending on the pattern character:
EXACT / DOT
T . . ?
diagonal
STAR ZERO
. . . T . ?
left by 2
STAR 1+
. T . ?
above
Step 04
Algorithm Walkthrough — Filling the DP Table
Let's trace through s = "aab", p = "c*a*b" step by step. We fill the table row by row, left to right.
Initialization
dp[0][0] = true — empty string matches empty pattern. Then for row 0 (empty string), we check if patterns like c*, c*a*, etc. can match the empty string by using the "star zero" rule.
""
c
*
a
*
b
""
T
F
T
F
T
F
a
F
?
?
?
?
?
a
F
?
?
?
?
?
b
F
?
?
?
?
?
Row 0: dp[0][2]=T because c* can match "" (skip c*). dp[0][4]=T because c*a* can match "" (skip both). dp[0][5]=F because "b" cannot match "".
Row 1: s = "a"
Now we process s[0] = 'a' against each pattern prefix:
""
c
*
a
*
b
""
T
F
T
F
T
F
a
F
F
F
T
T
F
a
F
?
?
?
?
?
b
F
?
?
?
?
?
dp[1][3]exact p[2]='a' == s[0]='a', so dp[1][3] = dp[0][2] = T
dp[1][4]star zero skip a*: dp[1][2] = F. star 1+ p[2]='a'==s[0]='a': dp[0][4] = T. Result: T
Completed Table
After filling all rows, the answer is at dp[3][5] — the bottom-right cell:
""
c
*
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
dp[3][5] = true — the string "aab" matches the pattern "c*a*b". The c* matches zero c's, a* matches two a's, and b matches b.
⚡
Why bottom-up works: Every cell only depends on cells above it, to its left, or diagonally above-left — all of which are already computed when we process row by row, left to right. No recursion needed.
Step 05
Edge Cases
Empty string with pattern
s = "", p = "a*" → true. The * can match zero occurrences, effectively consuming the entire pattern. Similarly p = "a*b*c*" matches empty because every character-star pair can match zero times.
Empty pattern
s = "a", p = "" → false. An empty pattern only matches an empty string. The DP base case handles this: dp[0][0] = true, but dp[i][0] = false for any i > 0.
Dot-star matches everything
s = "anything", p = ".*" → true. The . matches any single character, and * repeats it zero or more times. This is the "match everything" wildcard.
Multiple star groups
s = "aab", p = "c*a*b" → true. The c* matches zero c's, a* matches two a's, and b matches b. Each star group is processed independently in the DP table.
Star cannot stand alone
The pattern "*a" is invalid input per the problem constraints — * always follows a character or dot. The DP approach naturally handles this since we always look at p[j-1] when processing a * at position j.
Step 06
Full Annotated Code
classSolution {
publicbooleanisMatch(String s, String p) {
int m = s.length(), n = p.length();
// dp[i][j] = does s[0..i-1] match p[0..j-1]?boolean[][] dp = newboolean[m + 1][n + 1];
// Base case: empty string matches empty pattern
dp[0][0] = true;
// Initialize row 0: patterns like a*, a*b*, a*b*c*// can match the empty string by using zero occurrencesfor (int j = 1; j <= n; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2]; // skip the "x*" pair
}
}
// Fill the table row by rowfor (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
char sc = s.charAt(i - 1); // current string charchar pc = p.charAt(j - 1); // current pattern charif (pc == '.' || pc == sc) {
// Rule 1: exact match or dot wildcard// Both chars consumed → diagonal
dp[i][j] = dp[i - 1][j - 1];
} else if (pc == '*') {
// Rule 2: zero occurrences — skip "x*"
dp[i][j] = dp[i][j - 2];
// Rule 3: one or more occurrences// Only if the char before * matches s[i-1]if (p.charAt(j - 2) == '.'
|| p.charAt(j - 2) == sc) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
// else: mismatch, dp[i][j] stays false
}
}
return dp[m][n]; // answer: does entire s match entire p?
}
}
funcisMatch(s string, p string) bool {
m, n := len(s), len(p)
// dp[i][j] = does s[0..i-1] match p[0..j-1]?
dp := make([][]bool, m+1)
for i := range dp {
dp[i] = make([]bool, n+1)
}
// Base case: empty string matches empty pattern
dp[0][0] = true// Initialize row 0: patterns like a*, a*b*, a*b*c*// can match the empty string by using zero occurrencesfor j := 1; j <= n; j++ {
if p[j-1] == '*' {
dp[0][j] = dp[0][j-2] // skip the "x*" pair
}
}
// Fill the table row by rowfor i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
sc := s[i-1] // current string char
pc := p[j-1] // current pattern charif pc == '.' || pc == sc {
// Rule 1: exact match or dot wildcard → diagonal
dp[i][j] = dp[i-1][j-1]
} else if pc == '*' {
// Rule 2: zero occurrences — skip "x*"
dp[i][j] = dp[i][j-2]
// Rule 3: one or more occurrencesif p[j-2] == '.' || p[j-2] == sc {
dp[i][j] = dp[i][j] || dp[i-1][j]
}
}
// else: mismatch, dp[i][j] stays false
}
}
return dp[m][n] // answer: does entire s match entire p?
}
classSolution:
defis_match(self, s: str, p: str) -> bool:
m, n = len(s), len(p)
# dp[i][j] = does s[0..i-1] match p[0..j-1]?
dp = [[False] * (n + 1) for _ inrange(m + 1)]
# Base case: empty string matches empty pattern
dp[0][0] = True# Initialize row 0: patterns like a*, a*b*, a*b*c*# can match the empty string by using zero occurrencesfor j inrange(1, n + 1):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 2] # skip the "x*" pair# Fill the table row by rowfor i inrange(1, m + 1):
for j inrange(1, n + 1):
sc = s[i - 1] # current string char
pc = p[j - 1] # current pattern charif pc == '.'or pc == sc:
# Rule 1: exact match or dot wildcard# Both chars consumed → diagonal
dp[i][j] = dp[i - 1][j - 1]
elif pc == '*':
# Rule 2: zero occurrences — skip "x*"
dp[i][j] = dp[i][j - 2]
# Rule 3: one or more occurrences# Only if the char before * matches s[i-1]if p[j - 2] == '.'or p[j - 2] == sc:
dp[i][j] = dp[i][j] or dp[i - 1][j]
# else: mismatch, dp[i][j] stays Falsereturn dp[m][n] # answer: does entire s match entire p?
impl Solution {
pub fnis_match(s: String, p: String) -> bool {
let sb = s.as_bytes();
let pb = p.as_bytes();
let (m, n) = (sb.len(), pb.len());
// dp[i][j] = does s[0..i-1] match p[0..j-1]?let mut dp = vec![vec![false; n + 1]; m + 1];
// Base case: empty string matches empty pattern
dp[0][0] = true;
// Initialize row 0: patterns like a*, a*b*, a*b*c*// can match the empty string by using zero occurrencesfor j in1..=n {
if pb[j - 1] == b'*' {
dp[0][j] = dp[0][j - 2]; // skip the "x*" pair
}
}
// Fill the table row by rowfor i in1..=m {
for j in1..=n {
let sc = sb[i - 1]; // current string bytelet pc = pb[j - 1]; // current pattern byteif pc == b'.' || pc == sc {
// Rule 1: exact match or dot wildcard → diagonal
dp[i][j] = dp[i - 1][j - 1];
} else if pc == b'*' {
// Rule 2: zero occurrences — skip "x*"
dp[i][j] = dp[i][j - 2];
// Rule 3: one or more occurrencesif pb[j - 2] == b'.' || pb[j - 2] == sc {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
// else: mismatch, dp[i][j] stays false
}
}
dp[m][n] // answer: does entire s match entire p?
}
}
functionisMatch(s: string, p: string): boolean {
const m = s.length, n = p.length;
// dp[i][j] = does s[0..i-1] match p[0..j-1]?const dp: boolean[][] = Array.from({length: m + 1},
() => newArray(n + 1).fill(false));
// Base case: empty string matches empty pattern
dp[0][0] = true;
// Initialize row 0: patterns like a*, a*b*, a*b*c*// can match the empty string by using zero occurrencesfor (let j = 1; j <= n; j++) {
if (p[j - 1] === '*') {
dp[0][j] = dp[0][j - 2]; // skip the "x*" pair
}
}
// Fill the table row by rowfor (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const sc = s[i - 1]; // current string charconst pc = p[j - 1]; // current pattern charif (pc === '.' || pc === sc) {
// Rule 1: exact match or dot wildcard// Both chars consumed → diagonal
dp[i][j] = dp[i - 1][j - 1];
} else if (pc === '*') {
// Rule 2: zero occurrences — skip "x*"
dp[i][j] = dp[i][j - 2];
// Rule 3: one or more occurrences// Only if the char before * matches s[i-1]if (p[j - 2] === '.'
|| p[j - 2] === sc) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
// else: mismatch, dp[i][j] stays false
}
}
return dp[m][n]; // answer: does entire s match entire p?
}
Step 07
Interactive Demo
Enter a string and pattern, then step through the DP table cell by cell. Watch each rule in action.
DP Table Visualizer
Step 08
Complexity Analysis
Time
O(m × n)
Space
O(m × n)
We fill an (m+1) x (n+1) table, where m = |s| and n = |p|. Each cell takes O(1) work -- it checks the pattern character and looks up at most three previously computed cells (diagonal, left-by-2, or above). The table itself requires O(m × n) space.
Approach Breakdown
BRUTE RECURSION
O(2m+n) time
O(m+n) space
Each * operator branches into "use it" or "skip it" at every position, creating a binary recursion tree of depth m+n. The call stack grows up to O(m+n) deep, and overlapping subproblems are recomputed exponentially.
MEMOIZED DFS
O(m × n) time
O(m × n) space
Caches results for each (i, j) pair in a hash map, ensuring each of the m × n subproblems is solved at most once. The recursion stack still uses O(m+n) depth, and the memo table stores up to m × n entries.
BOTTOM-UP DP
O(m × n) time
O(m × n) space
Fills an (m+1) × (n+1) table iteratively, where each cell reads at most three previously computed neighbors (diagonal, left-by-2, or above) in O(1). No recursion overhead, and space can be reduced to O(n) with rolling arrays since each row only depends on the previous row.
🎯
The * operator is what makes this hard. Each * can match zero or more of the preceding element, causing exponential branching in naive recursion. The DP table collapses all that branching: each cell depends on at most 3 previous cells, and every (i, j) subproblem is solved exactly once. Space can be reduced to O(n) with rolling arrays since each row only reads from the current and previous row.
Coach Notes
Common Mistakes
Review these before coding to avoid predictable interview regressions.
State misses one required dimension
Wrong move: An incomplete state merges distinct subproblems and caches incorrect answers.
Usually fails on: Correctness breaks on cases that differ only in hidden state.
Fix: Define state so each unique subproblem maps to one DP cell.
Missing undo step on backtrack
Wrong move: Mutable state leaks between branches.
Usually fails on: Later branches inherit selections from earlier branches.
Fix: Always revert state changes immediately after recursive call.