一、题目描述
给你一个字符串s和一个字符规律p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖整个字符串s的,而不是部分字符串。
示例 1:
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
提示:
1 <= s.length <= 20
1 <= p.length <= 20
s 只包含从 a-z 的小写字母。
p 只包含从 a-z 的小写字母,以及字符 . 和 *。
保证每次出现字符 * 时,前面都匹配到有效的字符
二、解题思路
正则表达式匹配问题中存在大量的重叠子问题。在匹配过程中,一个较长的字符串与正则表达式的匹配结果,往往依赖于其前缀子串与正则表达式前缀子模式的匹配结果。这种重叠性使得动态规划成为了一个合适的解决方案,因为本题可以通过使用动态规划思想来存储已经计算过的子问题的解,避免了大量的重复计算。
1、确定dp数组及其含义
定义一个二维的dp数组
boolean[][] sp =new boolean[m][n];
m代表字符串s的长度,n代表字符串p的长度,则sp[i][j]就代表s中以i位置为结尾的子串和p中的j位置为结尾的子串是否匹配。如果匹配就是true,否则就是false。
2、对dp数组进行初始化
这里为了方便处理以及初始化,我们可以给字符串s和字符串p加一个相同的起始字符,比如空字符串,这样边很容易初始化出:
sp[0][0] = true;
3、确定dp数组的遍历顺序
从下图可以清晰地看到,我们的起始点是位置[0][0]。为了计算后续的值,它们都是基于这个初始位置[0][0]推导出来的。所以,我们只需要按照正序遍历即可顺利完成整个推导的过程。

4、确定状态转移方程式
这个是本题的一个难点,因为本题的状态转移方程式是需要分不同情况去考虑的。
1)当当前遍历的sp[i][j]位置不为*
sp[i][j] = sp[i-1][j-1]&&(s[i],p[j])
注意:(s[i],p[j])表示对应位置可以匹配的上,而对应位置可以匹配上的情况有:当前位置字符相等或者p对应位置为'.',即s[i]==p[j] || p[j] == '.'。此时我们为了方便,就直接使用(s[i],p[j])来表示对应位置可以匹配的上。后面表达式均用这种表示形式。
2)当当前遍历的sp[i][j]位置为*
因为 '*' 匹配零个或多个前面的那一个元素,那么又会有如下情况(我们以最多匹配2个来举例推导该情况下的最终状态转移方程式):
当匹配0个的时候
sp[i][j] = sp[i][j-2]
当匹配1个的时候
sp[i][j] = sp[i-1][j-2]&&(s[i],p[j-1])
当匹配2个的时候
sp[i][j] = sp[i-1][j-2]&&(s[i],p[j-1])&&(s[i-1],p[j-1])
因为最终的sp[i][j]其实就是上述三种情况只要有一个为true即可,所以
sp[i][j] = sp[i][j-2] || sp[i-1][j-2]&&(s[i],p[j-1]) || sp[i-1][j-2]&&(s[i],p[j-1])&&(s[i-1],p[j-1])
上述其实只是最多匹配两个的情况,还不是我们最终想要得到的状态转移方程式。
此时,我们可以将i = i-1带入上述表达式,得出:
sp[i-1][j] = sp[i-1][j-2] || sp[i-2][j-2]&&(s[i-1],p[j-1]) || sp[i-2][j-2]&&(s[i-1],p[j-1])&&(s[i-2],p[j-1])
所以,从上面两个表达式,我们不难得出该情况下的最终的状态转移方程式:
sp[i][j] = sp[i][j-2] || sp[i-1][j]&& (s[i],p[j-1])
注:这里是使用了逻辑运算的分配律:A && (B || C) 等价于 (A && B) || (A && C)
三、参考答案
根据上述分析,不难得出如下代码:
class Solution {
public boolean isMatch(String ss, String pp) {
//为ss和pp添加一个空字符头,方便后面dp数组的初始化
ss = " "+ss;
pp = " "+pp;
char[] s = ss.toCharArray();
char[] p = pp.toCharArray();
int m = s.length;
int n = p.length;
//1、确定dp数组sp[i][j]的含义: s中以i位置为结尾的子串和p中的j位置为结尾的子串是否匹配
boolean[][] sp =new boolean[m][n];
//初始化dp数组
sp[0][0] = true;
//遍历处理
for(int i = 0 ; i< m ; i++){
for(int j = 1 ; j < n ;j++){
//当当前j的位置不为*的时候,状态转移方程为:
//sp[i][j] = sp[i-1][j-1]&&(s[i],p[j]) //(s[i],p[j])表示对应位置可以匹配的上
if(p[j] != '*'){
sp[i][j] = i>=1 && sp[i-1][j-1]&&(s[i]==p[j] || p[j] == '.');
}else{ //此时的状态转移方程为:sp[i][j] = sp[i][j-2] || (sp[i-1][j] && (s[i],p[j-1]))
sp[i][j] = j>=2 && sp[i][j-2] || ( i>=1 && sp[i-1][j] && (s[i] == p[j-1] || p[j-1] == '.'));
}
}
}
return sp[m-1][n-1];
}
}