题目描述:
题解:
一、回溯
检查时,如果只考虑’.’,显然只需要同时在s和p中越过当前元素就可以了。
对于’*’,要分两种情况考虑:1、表示0个前面那个元素,这时在p中越过它和它前面那个元素,s不变;2、表示1个前面那个元素(可以扩展到n个),这时在s中越过当前元素,p不变。
引用官方解答的一句话:“当模式串中有星号时,我们需要检查匹配串 s 中的不同后缀,以判断它们是否能匹配模式串剩余的部分。一个直观的解法就是用回溯的方法来体现这种关系。”
递归实现:
bool isMatch(string s, string p) {
if (p.empty())return s.empty();
if ('*' == p[1])
return isMatch(s, p.substr(2)) || (!s.empty() && (s[0] == p[0] || '.' == p[0]) && isMatch(s.substr(1), p));
else
return !s.empty() && (s[0] == p[0] || '.' == p[0]) && (isMatch(s.substr(1), p.substr(1)));
}
复杂度分析:用T和P分别表示匹配串和模式串的长度,时间复杂度:O((T+P)2(T+P/2));空间复杂度:O((T+P)2(T+P/2))。
二、动态规划
着重解释DP,由简单到复杂。
我们用dp[i][j]表示s的前i个能否被p的前j个匹配;首先,只考虑’.’,很容易得到这样的状态转移方程:
if(s[i] == p[j] || '.' == p[j]) dp[i][j] = dp[i-1][j-1];
在此基础上,考虑这个问题:
s = "abc";
p = "a.c";
首先,定义dp数组,显然,因为有初始状态的存在,dp的大小应为(s.size()+1)*(p.size()+1):
初始化dp[0][0],即s、p都取空,显然dp[0][0] = true;初始化dp[0][j],1<=j<=3,显然,均为false;初始化dp[i][0],1<=i<=3,显然,均为false。
接着,应用状态转移方程,因为i=0,j=0,s[0]==p[0],所以dp[1][1]=dp[0][0]=true,注意这里dp数组的下标;
i=0,j=1,s[0]!=p[1],’.’==p[1],所以dp[1][2]=dp[0][1]=false;继续循环,逐次填充dp数组。
注意:程序实现时,在定义dp数组时,所有元素初始化为false。
令m=s.size(),n=p.size();显然,dp[m][n]=dp[3][3]=true即为结果。
将上述过程应用于:
s = "abc";
p = ".c";
得到:
显然,dp[m][n]=dp[3][2]=false即为结果。
只考虑’.’,代码:
bool isMatchOnly(string s, string p) {
if (p.empty()) return s.empty();
int m = int(s.size());
int n = int(p.size());
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
dp[0][0] = true;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (s[i] == p[j] || '.' == p[j])
dp[i + 1][j + 1] = dp[i][j];
}
}
return dp[m][n];
}
以上为只考虑’.‘的情况,应用动态规划看起来有点多余,但是一旦’*‘和’.'同时考虑,问题就复杂的多,动态规划的意义就体现出来了。
下面开始同时考虑:
if(s[i] == p[j] || '.' == p[j]) dp[i][j] = dp[i-1][j-1]; //'.' 显然,依旧成立。
if('*' == p[j]){ //'*'
//case 1:'*'的上一个字符!=s[i]与'.',显然这种情况下'*'只能代表0个上一个字符。
//例如s="ab",p="ac*b",则有:
//i=0,j=2时,检查"a"能否被"ac*"匹配:
//显然,此时*代表0个上一个字符,这种情况下:dp[i][j] = dp[i][j-2];
//即"a","ac*"->(状态转移至)"a","a";
//状态转移方程:
if(s[i] != p[j-1] && '.' != p[j-1]) dp[i][j] = dp[i][j-2];
//case 2:'*'的上一个字符==s[i]或'.':共有3种情况:'*'代表0,1,多个上一个字符
//以下三种情况都以s="abbbbbc",p="abb*bc"为例分析:
//case 2.1:i=1,j=3时,检查"ab"能否被"abb*"匹配:
//显然,此时'*'代表0个上一个字符,这种情况下:dp[i][j] = dp[i][j-2];
//即"ab","abb*"->(状态转移至)"ab","ab"
//case 2.2:i=2,j=3时,检查"abb"能否被"abb*"匹配:
//显然,此时'*'代表1个上一个字符,这种情况下:dp[i][j] = dp[i][j-1];
//即"abb","abb*"->(状态转移至)"abb","abb"
//case 2.3:i=3,j=3时,检查"abbb"能否被"abb*"匹配:
//显然,此时'*'代表2(多)个上一个字符,这种情况下:dp[i][j] = dp[i-1][j];
//即"abbb","abb*"->(状态转移至)"abb","abb*",
//这里观察到,case 2.3转移后的状态,刚好是case 2.2转移前的状态,
//再次应用case 2.3的结论,"abb","abb*"->(状态转移至)"ab","abb*"
//又刚好是case 2.1转移前的状态!
//这说明,如果'*'代表n个上一个字符,我们通过在待配串中减少末尾的一个字符(i-1),
//问题就归结(状态转移)为'*'代表n=n-1个上一个字符,直至n=0,归结为"0"(case 1或case 2.1);
//即,case 2.2是冗余的!
//注意:进入case 2的条件是:'*' == p[j] && (s[i] == p[j-1] || '.' == p[j-1])
//那么我们执行case 2.1,2.3中的哪一个呢?答案是:都执行,结果取或。
//why?因为'*'代表0个或多个前一个元素,
//执行case 2.1就相当于我们去验证“'*'代表0个前一个元素”这个命题对不对;
//执行case 2.3就相当于我们去验证“'*'代表多个前一个元素”这个命题对不对;
//显然,这两个命题在dp的相同位置,有且仅有一个为真(即'*'只能有一种含义)。
//我们把这两者的结果取或,即可保证真命题的结果的有效性(因为另一个假命题的结果一定是false)。
//状态转移方程:
if(s[i] == p[j-1] || '.' == p[j-1])
dp[i][j] = dp[i][j-2] /*|| dp[i][j-1]*/|| dp[i-1][j];
}
分析完毕,实战:
s = "abc";
p = "a.*";
初始化dp[0][0],即s、p都取空,显然dp[0][0] = true;初始化dp[0][j],1<=j<=3,显然,均为false;初始化dp[i][0],1<=i<=3,显然,均为false。
注意:假如s为空,p="a*"及类似组合,那么dp[0][j]会发生变化,需另做处理。
注意一点:p[0]不会为星号,因为没意义,不需要考虑;即填写dp[i][1]时,不会出现访问越界的情况。
继续填写dp数组:
显然,dp[m][n]=dp[3][3]=true即为结果。
完整代码:
bool isMatchDP(string s, string p) {
if (p.empty()) return s.empty();
int m = int(s.size()), n = int(p.size());
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
dp[0][0] = true;
//init: s="",p="a*"or".*"or...
for (int j = 1; j < n; j++){
if ('*' == p[j] && dp[0][j - 1] == true)
dp[0][j + 1] = true;
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (s[i] == p[j] || '.' == p[j])
dp[i + 1][j + 1] = dp[i][j];
if ('*' == p[j]) {
if (s[i] != p[j - 1] && '.' != p[j - 1])
dp[i + 1][j + 1] = dp[i + 1][j - 1];
else
dp[i + 1][j + 1] = dp[i + 1][j - 1]/* || dp[i + 1][j] */|| dp[i][j + 1];
}
}
}
return dp[m][n];
}
复杂度分析:两层循环,时间复杂度:O(mn);创建了二维dp数组,空间复杂度:O(mn)。