题目来源
题目描述
class Solution {
public:
bool isMatch(string s, string p) {
}
};
题目解析
分析题意
- str中一定不能含有
.
,*
exp
中:.
可以匹配任意一个字符*
:必须和exp前一个字符绑定使用(前一个字符可以是.
)- 可以消除前面的那个符号
- 可以什么也不干
- 可以复制任意次前面的个符号(1次,2次,3次)
- 因此
exp
中不能出现连续的两个*
因此,在匹配开始之前
先做一个大的过滤:
- str中一定不能含有
.
,*
exp
中不能出现连续的两个*
匹配是可以从前往后扫描,也可以从后往前扫描
(1)从前往后扫描的话:字符后面是否跟着*
会影响结果,分析起来比较复杂
(2)从后往前扫描的话
*
的前面一定有一个字符,*
也只影响这一个字符,它就像一个拷贝器
- s、p 串是否匹配,取决于:最右端是否匹配、剩余的子串是否匹配。
- 只是最右端可能是特殊符号,需要分情况讨论而已
样本对应模型
- 定义
dp[si][ei]
:str[si......]
能不能被exp[ei...]
匹配出来
base case:
- 当
exp
没有了,str
也必须没有了才是对得上 - 当
exp
没有了,但是str
还有没有匹配上的,那么就返回false
普通情况:
- expr有,str没有
- expr有,str有
怎么决策普通情况呢?根据ei
的下一个位置是不是*
来决策
- 如果
e
i
+
1
ei+1
ei+1不是
*
-
e
i
+
1
ei+1
ei+1不是
*
有哪些情况呢?- e i + 1 = = e . l e n ei + 1 == e.len ei+1==e.len,说明 e i ei ei和 s i si si是最后的字符了
- e [ e i + 1 ] ! = ∗ e[ei + 1] != * e[ei+1]!=∗
- 怎么决策?
- 如果
e
i
+
1
ei+1
ei+1不是
*
,就说明 e i ei ei没有操作空间了,就是说后面没有*
可以将 e i ei ei变没 - 此时
s
i
si
si必须能和
e
i
ei
ei对上。这意味着:
- s i ! = s . l e n si != s.len si!=s.len
- 而且,下面情况之一必须成立:
- e [ e i ] = = s [ s i ] e[ei] == s[si] e[ei]==s[si]
- e [ e i ] = = . e[ei] == . e[ei]==.
- 然后对继续下一个字符匹配: s [ s i + 1 ] 与 e [ e i + 1 ] s[si + 1]与e[ei+1] s[si+1]与e[ei+1]
- 如果
e
i
+
1
ei+1
ei+1不是
-
e
i
+
1
ei+1
ei+1不是
- 如果
e
i
+
1
ei+1
ei+1是
*
,那么可以使用*
对 e [ e i ] e[ei] e[ei]消除,复制、什么也不干- 如果
e
[
e
i
]
!
=
s
[
s
i
]
e[ei] != s[si]
e[ei]!=s[si],那么必须用
*
将 e [ e i ] e[ei] e[ei]消除,因此,下一步 s [ s i ] s[si] s[si]和 e [ e i + 2 ] e[ei+2] e[ei+2]匹配 - 如果 e [ e i ] = = s [ s i ] e[ei] == s[si] e[ei]==s[si],那么可以用来消除零个或者一个或者全部的 s [ s i ] s[si] s[si]
- 如果
e
[
e
i
]
!
=
s
[
s
i
]
e[ei] != s[si]
e[ei]!=s[si],那么必须用
class Solution {
bool isValid(string s, string e){
// s中不能有'.' or '*'
for (int i = 0; i < s.size(); ++i) {
if(s[i] == '.' || s[i] == '*'){
return false;
}
}
// 开头的e[0]不能是'*',没有相邻的'*'
for (int i = 0; i < e.size(); ++i) {
if(e[i] == '*' && (i == 0 || e[i - 1] == '*')){
return false;
}
}
return true;
}
// str[si.....] 能不能被 exp[ei.....]配出来! true false
bool process(string s, string e, int si, int ei){
if(ei == e.length()){
return si == s.length();
}
// exp[ei]还有字符
// ei + 1位置的字符,不是*
if(ei + 1 == e.length() || e[ei + 1] != '*'){
// ei + 1 不是*
// str[si] 必须和 exp[ei] 能配上!
return si != s.length() && (e[ei] == s[si] || e[ei] == '.') && process(s, e, si + 1, ei + 1);
}
// exp[ei]还有字符
// ei + 1位置的字符,是*
while (si != s.length() && (e[ei] == s[si] || e[ei] == '.')) {
if (process(s, e, si, ei + 2)) {
return true;
}
si++;
}
return process(s, e, si, ei + 2);
}
public:
bool isMatch(string s, string e) {
if(e.empty() || e.empty()){
return false;
}
return isValid(s, e) && process(s, e, 0, 0);
}
};
记忆化搜索
class Solution {
bool isValid(string s, string e){
// s中不能有'.' or '*'
for (int i = 0; i < s.size(); ++i) {
if(s[i] == '.' || s[i] == '*'){
return false;
}
}
// 开头的e[0]不能是'*',没有相邻的'*'
for (int i = 0; i < e.size(); ++i) {
if(e[i] == '*' && (i == 0 || e[i - 1] == '*')){
return false;
}
}
return true;
}
// str[si.....] 能不能被 exp[ei.....]配出来! true false
bool process(string s, string e, int si, int ei, std::vector<std::vector<int>> &dp){
if (dp[si][ei] != 0) {
return dp[si][ei] == 1;
}
bool ans = false;
if(ei == e.length()){
ans = si == s.length();
}else{
// exp[ei]还有字符
// ei + 1位置的字符,不是*
if(ei + 1 == e.length() || e[ei + 1] != '*'){
// ei + 1 不是*
// str[si] 必须和 exp[ei] 能配上!
ans = si != s.length() && (e[ei] == s[si] || e[ei] == '.') && process(s, e, si + 1, ei + 1);
}else{
if(si == s.length()){
ans = process(s, e, si, ei + 2, dp);
}else{ // si没结束
if (s[si] != e[ei] && e[ei] != '.') {
ans = process(s, e, si, ei + 2, dp);
} else { // s[si] 可以和 e[ei]配上
ans = process(s, e, si, ei + 2, dp) || process(s, e, si + 1, ei, dp);
}
}
}
}
dp[si][ei] = ans ? 1 : -1;
return ans;
}
public:
bool isMatch(string s, string e) {
if(e.empty() || e.empty()){
return false;
}
std::vector<std::vector<int>> dp(s.length() + 1, std::vector<int>(e.length() + 1));
// dp[i][j] = 0, 没算过!
// dp[i][j] = -1 算过,返回值是false
// dp[i][j] = 1 算过,返回值是true
return isValid(s, e) && process(s, e, 0, 0, dp);
}
};
状态转移方程
(1)定义状态
- d p [ i ] [ j ] dp[i][j] dp[i][j]:s的前 i i i个字符和 p p p的前 j j j个字符能否匹配
(2)状态转移
在进行状态转移时, s s s中的字符是固定不变的,我们考虑 p p p的第 j j j个字符与 s s s的匹配情况
- p [ j ] p[j] p[j]是一个小写字母 a − z a-z a−z,则 s [ i ] s[i] s[i]必须也是小写字母而且与 p [ j ] p[j] p[j]相同
2.
p
[
j
]
=
=
.
p[j] == .
p[j]==.,则
p
[
j
]
p[j]
p[j]一定可以与
s
[
i
]
s[i]
s[i]匹配成功,此时:
- p [ j ] = = ∗ p[j] == * p[j]==∗,则 p [ j ] p[j] p[j]可以匹配(复制)其前一个字符 p [ j − 1 ] p[j - 1] p[j−1]任意次(包括0次)。
举个例子,以下图 s= "abaaacd",p="aba*cd",s[i]='a',p[j-1]='a'
和p[j]='*'
为例
- 匹配
0
次,意味着 p [ j − 1 ] p[j - 1] p[j−1]和 p [ j ] p[j] p[j]不起作用,相当于在 p p p中删除了 p [ j − 1 ] p[j - 1] p[j−1]和 p [ j ] p[j] p[j],此时有:
- 匹配 1 次,意味着
p
[
j
−
1
]
p[j - 1]
p[j−1]和
p
[
j
]
p[j]
p[j]组成了
a
,此时,如果 s [ i ] = = p [ j − 1 ] s[i] == p[j - 1] s[i]==p[j−1],则 d p [ i ] [ j ] dp[i][j] dp[i][j]可以由 d p [ i − 1 ] [ j − 2 ] dp[i-1][j-2] dp[i−1][j−2]转移而来,此时有:
- 匹配 2 次,意味着
p
[
j
−
1
]
p[j - 1]
p[j−1]和
p
[
j
]
p[j]
p[j]组成了
aa
,如果s[i - 1,j] = p[j - 1],则有:
- 匹配3次,意味着
p
[
j
−
1
]
p[j - 1]
p[j−1]和
p
[
j
]
p[j]
p[j]组成了
aaa
,如果s[i - 2,i - 1,j] = p[j - 1],则有:
- 匹配k次,意味着
p
[
j
−
1
]
p[j - 1]
p[j−1]和
p
[
j
]
p[j]
p[j]组成了k个
a
,如果s[i - k + 1,…,j] = p[j - 1],则有:
上述过程对应的状态更新过程如下图所示:
状态转移的优化:总的来看,当
p
[
j
]
=
=
∗
p[j] == *
p[j]==∗时,对于匹配0~k
次,我们有
同时,对于 dp[i-1][j]我们有:
观察发现,(2)式与(1)式中的后 kk 项刚好相差了一个条件 s[i]=p[j-1],将(2)式代入(1)式可得简化后的「状态转移方程」为:
p[j]= '*'时,简化后对应的状态更新过程如下图所示:
总结以上各种情况来看,最终的状态转移方程如下:
从坐标的角度来看,对应的状态更新过程如下图所示: