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.
Solutions
Solution 1: Memoization Search
Thinking
Matching left to right and, at each *, trying \(0,1,2,\ldots\) repetitions is the natural search. \(s\) and \(p\) are at most length \(20\), so exponential time sometimes passes, but the same pair of positions is recomputed over and over.
A * applies only to the preceding token: either match it \(0\) times (skip two pattern indices), or the current character fits that token (including .) and the same * consumes the next character of \(s\). A plain character must be consumed one-for-one.
The state is therefore “can suffix \(i\) of \(s\) match suffix \(j\) of \(p\)”. Search with memoization so exponential branches collapse to \(O(mn)\) pairs \((i,j)\).
We design a function \(dfs(i, j)\), which indicates whether the \(i\)-th character of \(s\) matches the \(j\)-th character of \(p\). The answer is \(dfs(0, 0)\).
The calculation process of the function \(dfs(i, j)\) is as follows:
If \(j\) has reached the end of \(p\), then if \(i\) has also reached the end of \(s\), the match is successful, otherwise, the match fails.
If the next character of \(j\) is '*', we can choose to match \(0\)\(s[i]\) characters, which is \(dfs(i, j + 2)\). If \(i \lt m\) and \(s[i]\) matches \(p[j]\), we can choose to match \(1\)\(s[i]\) character, which is \(dfs(i + 1, j)\).
If the next character of \(j\) is not '*', then if \(i \lt m\) and \(s[i]\) matches \(p[j]\), it is \(dfs(i + 1, j + 1)\). Otherwise, the match fails.
During the process, we can use memoization search to avoid repeated calculations.
The time complexity is \(O(m \times n)\), and the space complexity is \(O(m \times n)\). Here, \(m\) and \(n\) are the lengths of \(s\) and \(p\) respectively.
Memoized Solution 1 is already \(O(mn)\), but it is still recursion: implicit stack depth and larger constants. The transitions have no aftereffect, so we can fill \(i,j\) in increasing order.
\(f[i][j]\) is whether the first \(i\) characters of \(s\) match the first \(j\) of \(p\). Empty matches empty. We also need \(i=0\) so patterns like a* can match the empty string, which is why the outer loop starts from the empty prefix.
The * cases are the same as Solution 1: skip two pattern characters, or consume one more character of \(s\) when it fits. After filling the table we read \(f[m][n]\).
We can convert the memoization search in Solution 1 into dynamic programming.
Define \(f[i][j]\) to represent whether the first \(i\) characters of string \(s\) match the first \(j\) characters of string \(p\). The answer is \(f[m][n]\). Initialize \(f[0][0] = true\), indicating that the empty string and the empty regular expression match.
Similar to Solution 1, we can discuss different cases.
If \(p[j - 1]\) is '*', we can choose to match \(0\)\(s[i - 1]\) characters, which is \(f[i][j] = f[i][j - 2]\). If \(s[i - 1]\) matches \(p[j - 2]\), we can choose to match \(1\)\(s[i - 1]\) character, which is \(f[i][j] = f[i][j] \lor f[i - 1][j]\).
If \(p[j - 1]\) is not '*', then if \(s[i - 1]\) matches \(p[j - 1]\), it is \(f[i][j] = f[i - 1][j - 1]\). Otherwise, the match fails.
The time complexity is \(O(m \times n)\), and the space complexity is \(O(m \times n)\). Here, \(m\) and \(n\) are the lengths of \(s\) and \(p\) respectively.