对于p字符串有点、字母、点*、字母*四种元素,点匹配任意一个字母,字母匹配相同的一个字母,点*匹配任意字母(可以是任意不同字母,例如.*匹配abc),字母*匹配连续任意个相同字母,值得注意的是*的任意包括0个。由于*可以匹配任意个,造成检验s和p是否完全匹配的时候难以确定究竟*匹配几个字母合适,这正是本题的关键点。题意简单粗暴,看一下原题,然后分析一下如何处理。
Given an input string (
s
) and a pattern (p
), implement regular expression matching with support for'.'
and'*'
.'.' Matches any single character. '*' Matches zero or more of the preceding element.
The matching should cover the entire input string (not partial).
Note:
s
could be empty and contains only lowercase lettersa-z
.p
could be empty and contains only lowercase lettersa-z
, and characters like.
or*
.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 precedeng 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 (.)".
Example 4:
Input: s = "aab" p = "c*a*b" Output: true Explanation: c can be repeated 0 times, a can be repeated 1 time. Therefore it matches "aab".
Example 5:
Input: s = "mississippi" p = "mis*is*p*." Output: false
一、动态规划
开始前的准备工作
当p中的a*遇到s中的aaa的时候,由于事先不知道那哪种匹配可能是正确的或者有几种是正确的,应该尝试匹配空、a、aa、aaa等全部可能的情况,而动态规划实际上就是一种穷举所所有情况的好方法。假设s有m个元素,p有n个元素,这里我选择建立dp[m+1][n+1]数组,之所以多1排1列是为了处理首排首列的时候方便一些,减少特殊情况的单独列出。横排m+1代表空白一个首元素+s中的m个元素,竖排n+1代表空白一个元素+p中的n个元素,相当于给s和p开头加了一个可有可无的空白元素。先处理匹配s前面加的空白元素匹配问题,p可以使用任意空白去匹配(加的空白元素和.*和字母*等),如果匹配,对应位为true。
然后重点处理s中的m个元素匹配
每一次匹配结果受三个方向(dp二维数组)影响,例如aab和c*a*b,假如当前在匹配前者的b和后者的a。
1、上方向代表s当前元素和p上一个元素的匹配情况(即b和*),如果当前p元素是*,而*的前一个元素匹配了s的当前元素,则加上*也一定匹配,这种匹配方式等于用字母*或.*只匹配一个元素。上方向也可以代表s当前元素和p上两个元素匹配情况(即a和c是否匹配),这种匹配方式等于把字母*或.*一并拿出来,匹配0个元素,如果匹配,则加上字母*或.*这个空白也一定匹配。如果当前p元素不是*不用参考此方向。
2、左方向代表s上一个元素和p当前元素的匹配情况(即a和b),如果当前p元素是*,而字母*或.*已经匹配s的上一个元素,又能匹配s的当前元素,就可以把字母*或.*的匹配数量延长一个,到达2或更多。至此就包括了字母*或.*匹配0个、1个或多个元素的全部情况。
3、左上方向代表s上一个元素和p上一个元素的匹配情况(即a和*),如果当前元素是字母或者.,也就是说不是*,则若此处匹配,必须是p的上一个元素匹配s的上一个元素且p的当前元素也匹配s的当前元素。
检查一遍已经包括了所有的情况,没有遗漏。其中1和2处理*的匹配,这是最复杂的情况,3处理字母和.的匹配,比较容易。
对照一下代码和注释,可以把代码和以上三种方向对照一下。
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.size(),n = p.size();
vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));
dp[0][0] = true;
for (int j = 1;j < n && p[j] == '*';j+=2)
dp[0][j+1] = true;
for (int i = 0;i < m;i++)
for (int j = 0;j < n;j++){
if (p[j]!='*')
//如果是字母或.只受左上方向影响,即方向3
dp[i+1][j+1] = p[j]=='.' ? dp[i][j] : (dp[i][j] && (p[j]==s[i]));
else
//如果是*则受左和上两个方向影响,即方向1和2,有三个或分别对应上述三种情况
dp[i+1][j+1] = dp[i+1][j-1] || dp[i+1][j] || (dp[i][j+1]&&((p[j-1]==s[i])||p[j-1]=='.'));
}
return dp[m][n];
}
};
二、递归
对于*可以匹配任意数量元素的不确定性也可以用递归处理,可不可行试过才知道,通过深入下一层递归判断。所以有三大类情况:匹配、不匹配、不知道需要深入判断。其中细分情况比较多,我放在代码注释里一一解释。记住一点,只有匹配到s和p都没有剩余元素了才算成功。
bool match(string& s,int i,string& p,int j){
if (i==s.size() && j==p.size()) //如果s和p都没有剩余元素了,则匹配成功
return true;
if (j==p.size()) //如果s还有元素但p没了,则匹配失败
return false;
if (j+1<p.size() && p[j+1]=='*'){ //如果p的下一个元素是*,当作整体处理,分为.*和字母*讨论
if(p[j]=='.'){ //如果是.*
for (int pos = i;pos <= s.size();pos++) //穷举匹配0、1、多个元素的情况,有一个匹配就成功,都失败则彻底失败
if (match(s,pos,p,j+2)) //注意这里不能return match(),否则穷举不了后面的情况
return true;
return false;
}
else{ //如果是字母*
if(match(s,i,p,j+2)) return true; //匹配0个元素,等于跳过字母*,同样不能return match()
for (int pos = i;pos<s.size()&&s[pos]==p[j];pos++) //匹配1、多个元素,比.*多考虑的是只有当前的一个匹配上了,才能考虑匹配更多的元素
if (match(s,pos+1,p,j+2))
return true;
return false;
}
}
else{ //如果是字母或.,匹配上了的话深入下一层看能否成功,此处终于可以return match()了
if (i<s.size() && (s[i]==p[j] || p[j]=='.'))
return match(s,i+1,p,j+1);
else
return false;
}
}
class Solution {
public:
bool isMatch(string s, string p) {
return match(s,0,p,0);
}
};
个人认为动态规划代码简洁漂亮,内涵却不简单,两句话写尽人世铅华,递归写的比较粗糙,又臭又长,胜在情况分类比较清晰。最后欢迎大家留言讨论,如有错误或改进还请不吝赐教。