文章目录
dp 21 正则表达式匹配
牛客版本
描述
请实现一个函数用来匹配包括’.‘和’‘的正则表达式。模式中的字符’.'表示任意一个字符,而 ’ * '表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"aba"均不匹配
leetcode版本
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ’ * ’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
这题首先得理解对
.是匹配任意一个字符 这并没有什么问题
关键是*
*是表示它前面的字符可以出现任意次
包括了0次
这也就意味着 匹配的时候如果出现
x* 这种 那么它可以被直接跳过。
也就是说 a 和 c*a 是匹配的
因为c* 组合在一起可以 被跳过。
最后的a匹配上了头上的a
所以匹配通过。
样例:
"aaa"
"a*a"
true
"aa"
"a"
false
"aa"
"a*"
true
"ab"
".*"
true
"aab"
"c*a*b"
true
"a"
".*"
true
思路
此题有两种解法。动态规划或者状态机。
这两种解法的关键都在于状态的转换。
下面先讲述动态规划的解法。
首先定义状态dp(i,j) : s串前 i 个字符和 p串前 j 个字符是否匹配。
很自然的有:
d
p
(
i
,
j
)
=
d
p
(
i
−
1
,
j
−
1
)
,
i
f
s
[
i
]
=
=
p
[
j
]
∣
∣
p
[
j
]
=
=
′
.
′
dp(i,j) = dp(i-1,j-1) , if \space s[ i ] == p[ j ] \space||\space p[ j ] == '.'
dp(i,j)=dp(i−1,j−1),if s[i]==p[j] ∣∣ p[j]==′.′
如果不匹配则为false.
但是此题不能简单地不匹配就判定为false。因为存在 * ,这可以和当前不匹配的组合,去掉了当前不匹配的元素。
接下来讨论 * 的情况。
如果第 j 个字符是 * 。
对于匹配0次的情况,那么当前状态dp[ i ] [ j ] |= dp[ i ] [ j-2 ]。也就是说当前状态直接跳过了这两个元素。
对于匹配1次的情况,那么当前状态dp[ i ] [ j ] |= dp[ i-1 ] [ j-2 ]。也就是说当前状态匹配了s中的一个元素。
对于匹配2次的情况,那么当前状态dp[ i ] [ j ] |= dp[ i-2 ] [ j-2 ]。也就是说当前状态匹配了s中的两个个元素。
…
这样去枚举,情况就太多了。
这便是这题最重要的点,解决这个问题就解决了这题。
我们是无法确定 字符加 * 到底匹配多少次的。
如果每次都去看它到底比较成功了多少次,会提高算法的复杂度。而且代码也不好写。(这样就需要去特判情况)
于是这里转变了思路,不再考虑当前字符到底匹配了多少次?
而去考虑当前字符是否和结尾字符匹配。匹配的话,当前这两个字符就还可以继续匹配,不然的话就匹配失败,跳过这两个字符。
状态转移方程如下
dp[ i ] [ j ] |= dp[ i ] [ j-2 ] if not match
dp[ i ] [ j ] |= dp[ i-1 ] [ j ] || dp[ i ] [ j-2 ] if match
对于
aaab 和 a*b
我们首先匹配了a, 然后遇到 * ,这个时候我们发现是匹配的,因此其匹配成功一次,s指针后移,p指针不动。
而后继续如此匹配。直接匹配到b,发现匹配不上了,p指针后移,成功匹配。
按照上述思路写出来的代码如下
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size(), m = p.size();
vector<vector<int> > dp(n + 1, vector<int>(m + 1));
auto matches = [&](int i, int j) { // 方便后续判断
if (i == 0) {//如果s串长度都为0了 还怎么匹配 所以直接返回false
return false;
}
if (p[j - 1] == '.') {
return true;
}
return s[i - 1] == p[j - 1];
};
dp[0][0] = true;//注意初始化
for (int i = 0; i <= n; ++i) {//s的长度必须从0开始 因为 c* 这种可以匹配长度为0的s子串
for (int j = 1; j <= m; ++j) {
if (p[j - 1] == '*') {
dp[i][j] = dp[i][j - 2];//这样写是为了简化代码
if(matches(i,j-1))
dp[i][j] |= dp[i-1][j];
}else {
if (matches(i,j)) {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
}
return dp[n][m];
}
};
此题还有第二种方法,状态机的方法。
状态机的方法比动规的版本更考验对状态之间的变换。因为要能真正使用成功这种方法,必须对状态和它们之间的变换一清二楚。
根据上述状态转换方程,
当 s[i] matches p[j] 时,我们就可以继续匹配 s[i+1] 和 p[j+1]
而当遇到p[j] == ‘*’ 时,我们就要去判断p[j-1]和 s[i] 是否匹配,如果匹配,那么p指针不动,继续匹配,s指针后移一位。
否则p指针后移一位,s指针后移一位。
但是按照如此逻辑实现代码,一些边界判断就会变得十分讨厌。
比如你当前p 有一个字符不匹配s的字符,你还得去看看p后面是不是有*,然后再根据此判断是直接判定失败还是继续走。
所以我们直接判断 *(P+1) 是否为 *,这样就能省去很多不必要的麻烦。
注意此时,当s到达字符串结尾了,但是p没有到达字符串结尾,不能简单地判定为false,因为可能存在p后续都能被跳过的情况。
如果觉得自己很深刻的理解状态转移方程,就可以试着写写状态机版本的代码,检验一下理解的程度。
具体代码实现如下
bool isMatch(char * s, char * p){
//printf("%c %c\n", *s, *p);
if(*p == '\0' && *s == '\0') {//同时结尾 true
return true;
}
if(*s && !*p) {//s没完 p完了 false
return false;
}
if(*(p+1) == '*') {
if(*p == *s || (*s &&*p == '.')) {//匹配0次或 多次 此处匹配成功后 匹配0次的意义在于 为后续的字符串匹配让路 在动规版本中 我们其实也是如此匹配的 但是并没有此处明显 考虑如下例子 aaa a*aaa
return isMatch(s, p + 2) || isMatch(s+1, p);
}
return isMatch(s, p+2);//匹配失败 直接跳过
}
if((*s && *p == '.') || *s == *p) {//如果是 . 匹配任意字符
return isMatch(s + 1, p + 1);//双方后移
}
return false;
}
再附送一个特别精简的代码
bool isMatch(char * s, char * p){
if(*p == 0)
return *s == 0;
bool first_match = *s && (*s == *p||*p == '.');
if(*(p+1) == '*')
return isMatch(s,p+2)||(first_match && isMatch(s+1,p));
return first_match && isMatch(s+1,p+1);
}
感想
小白拦路虎。
状态转移方程不是难想,而是难改。
但是一旦转变了思路,此题就豁然开朗了。
这题关键之处正在于 p指针不动, s指针后移。这样就不需要去判断多少次了。这正是一种分解的思想。
不管它成功匹配了多少次(k),它必然是由于我一次次比较判定才得到的结果。