输入两个字符串:s和p,判断s能否匹配p。
这俩题要求差不多,44题最开始我没用DP,一点一点循环匹配,结果最多过了1600+testcase,剩下的过不去,因为循环匹配无法覆盖所有的边界条件。当时很难受,因为边界条件有很多,我一个一个测出来然后补救,最终发现有一类边界条件无法补救,给我这思路判了死刑。但是没法子。就搁置在一边。现在开始学DP,就把这道题拎出来又做了一遍。没有看Solution,直接自己分析做的。时空复杂度我自己都看乐了。太差了:
这是10题:
这是44题:
简直是我AC的题目里面时空复杂度最差的。但好歹是自己想出来了,还是写下来吧。
对这俩题来说,我的思路是类似的。因此以44题来讲述我的思路,10题的区别之后再说。
44题中,p中可以有三种元素:a~z,一个匹配一个;?,匹配一个任意的;*,匹配从空字符到任意长度的字符串,万能。
我之前有点DP的PTSD的,就是那种,一眼看不出用什么方法做的,或者是特别特别麻烦,很可能是字符串题,八成就是DP。一猜一个准。很无奈。
既然是DP,关键就是找递推关系式了。在这部分我先说一下我自己找到的关系式。差是很差,但是至少做出来了不是。
当时我是这样想的:
看见s=“mississippi”,p=“m**is*p*”。于是就开始用我那点贫乏的DP经验:我得找一个符合条件的结果,然后在它基础上扩展。
好嘞。先看s的第一个字符,m;再看p的第一个字符,也是m。这不就是第一个符合条件的结果。
然后看s的第二个字符,i;再看p的第二个字符,*,万能字符。也匹配。这就是第二个符合条件的结果。开始猜想:递推公式是不是就是“长度为i的字符串符合条件,要求长度为i-1的字符串符合条件且第i个字符和p中的字符匹配”?不知道,接着往下看。
之后看s的第三个字符,s;再看p的第三个字符,*,万能字符。也匹配。好像对啊。不对!为什么不对,凭什么s的第三个字符要和p的第三个字符匹配啊,人家p的第二个字符为*,有排面,可以和任意长度的字符匹配,你只给它匹配一个字符,这不是屈才了。那按这个意思,*可以和之后所有字符匹配咯?是啊,所以说“m*”这个字符串,就可以匹配上面的输入了。
有点意思。但还是迷糊。接着往下看。
s的第四个字符,s,再看p的第四个,i。等会,p为啥要看第四个,你p第二个有排面,已经能把s的所有字符都匹配完了。那我啥时候才能看到p的第四个字符啊,它对s有限制啊。咱们看一看i能匹配s中的哪些字符吧:p的第四个是i,s的第二个、第五个、第八个、第十一个是i。那p的这个i在什么情况下能匹配上s的这几个i呢?这就是递推公式的核心问题。
从头开始捋,先看p的i能不能匹配s的第五个i。什么时候能匹配?“p的i之前的字符串能和s的前四个字符串匹配”。这时候才能轮到p的i匹配s的第五个i。那到底能不能匹配呢?“miss”和“m**”,肯定能。
然后看p的i能不能匹配s的第八个i。同样的,什么时候能匹配?“p的i之前的字符串能和s的前七个字符串匹配”。这时候才能轮到p的i匹配s的第八个i。那到底能不能匹配呢?“mississ”和“m**”,肯定能。有点眉目了。
第十一个也一样。能匹配。我们来总结一下,p的i能匹配到这几个位置的元素,都是因为p的i之前的字符串和这几个位置之前的字符串已经匹配了。由于*实在太厉害,能和很多字符匹配,所以i也能匹配好几个。
也就是说,我们把所有的i能匹配的s的子串找出来,在这些子串基础上再往后找,直到找到最后?
是这么个理。
有点模糊啊,我怎么知道这俩修饰词这么多的字符串匹不匹配啊。用DP表。这该如何记录?
DP表如何表达出“p的i之前的字符串和这几个位置之前的字符串已经匹配”呢?我们已经知道DP表是一个二维数组(不要问我为什么,我加一块就做了三道DP题,全是二维数组当DP表= =),这个元素有什么说道么?
先把s放在横坐标,那纵坐标也得有点东西啊,先把p的第一个字符放上吧,这m和s的第一个字符匹配啊,加个一标记它俩匹配不过分吧:
那纵坐标第二个我加个*,第三个我也加个*,*和m之后的所有元素都匹配,按上面的理论,这些我都得写个1?那就写呗。
注意由于*可以匹配空元素,因此第二个*也可以和i匹配。
p的第四个i按上面我们的分析写出来:
看不出什么来啊。。。那就再写一个,p的第五个,s。这个s得和哪个匹配呢?我们要看s之前的“m**i”和s的哪些字符串匹配。观察上图,“m**i”和“mi”、“missi”、“mississi”、“mississippi”都匹配。诶?看这几个字符串,不就是之前问题“那p的这个i在什么情况下能匹配上s的这几个i呢?”的答案么。那s能匹配哪些字符就可以知道了:
接下来把p的所有字符放在纵坐标:
当我们把所有的1填入,我们也就得到了递推公式:
DP[i][j]=(DP[i-1][j-1]==1)&&(p[i]==s[j])
成了。那如何判断s和p能不能匹配呢?我们来看一下p中所有能和s匹配的子串在DP中的特点:
m*,和s匹配;m**、m**i、m**i*、m**is*p*。所有的都在这里了。在想一下DP表中1的含义:DP[i][j]=1,表示p的第i个和s的第j个字符匹配,而且它们之前的子串也匹配。那好了,只要最后的DP[p.size][s.size]==1不就说明都匹配么。拿上面几个子串验证一下:嚯,最后果然都是1。这不就解出来了么。代码如下:
class Solution {
int DP[1050][1050]={0};
public:
bool isMatch(string s, string p) {
if(s==""&&(p==""||p=="*"))
return true;
if(s==""&&p.size()>0)
return false;
if(s.size()>0&&p=="")
return false;
if(p[0]=='*')
for(int j=0;j<=s.size();j++)
DP[0][j]=1;
else if(p[0]=='?'||p[0]==s[0])
DP[0][1]=1;
else
return false;
for(int i=1;i
注意以下几点:
第一,递推的初始条件,也就是DP的第一行是要我们自己提前写出来的:分情况填入不同个数个1即可;
第二,DP[i-1][j-1]不好实现,对于j=0的情况不友好,因此我将递推条件改为
DP[i][j+1]=(DP[i-1][j]==1)&&(p[i]==s[j])了。对应的判断成功匹配也要重新改一下。
我这种时空复杂度都是O(mn),可以说是很差了。
10题和44题不同的一点是,*代表它前面的元素会重复0~n次,.和44题中的?一样。这个*的不同可折磨死我了。最后我用了一种很笨的方法来转换:
首先,将两个及以上的*压缩为一个*。由于*的特性,你写个a**和a*没有区别。
其次,对于两个字符,如果后面的是*,那么就相当于把前面的字符“强化”,比如说“a*”,强化为“A”,A可以匹配任意长度的a。“.*”就牛逼了,和44题中的*一样,能匹配所有字符串,我们将其强化为“?”。
最后,用强化后的p去和s匹配。匹配算法类似44题。代码如下:
class Solution {
int DP[1050][1050]={0};
public:
bool isMatch(string s, string p) {
if(p==".*")
return true;
if(s==""&&(p==""))
return true;
if(s.size()>0&&p=="")
return false;
string new_p="";
int i=0;
while(p[i]=='*')
i++;
for(;i='a'&&new_p[i]<='z')
{
last_p+=new_p[i]-'a'+'A';
i++;
}
else if(new_p[i]=='.')
{
last_p+='?';
i++;
}
}
}
if(new_p[new_p.size()-1]!='*')
last_p+=new_p[new_p.size()-1];
cout<='a'&&last_p[0]<='z')
{
if(last_p[0]==s[0])
DP[0][1]=1;
else
return false;
}
else
{
DP[0][0]=1;
for(int i=0;i<=s.size();i++)
{
if(last_p[0]-'A'+'a'==s[i])
{
DP[0][i+1]=1;
}
else
break;
}
}
for(int i=1;i='a'&&last_p[i]<='z')
DP[i][j+1]=((DP[i-1][j]==1)&&(s[j]==last_p[i]));
else if((last_p[i]>='A'&&last_p[i]<='Z')&&(DP[i-1][j]==1))
{
while(DP[i-1][j]==1)
{
DP[i][j]=1;
j++;
}
j--;
while(j<=s.size())
{
DP[i][j+1]=((s[j]==last_p[i]-'A'+'a'));
if(DP[i][j+1]==0)
break;
j++;
}
}
}
}
for(int i=0;i
太丑陋了,真是太丑陋了,我这道题应该是迄今为止我自己error最多的了,我测出了不下十个测试样例。哭瞎。
最难的是A应该如何根据上文对DP进行赋值:由于A可以为0,所以事情很难办:我们以s=“mississippi”,p=“mis*is*p*.”为例:
我为了规避j=0的情况,将DP中为1的含义进行了改变:在之前,DP[i][j]==1表示s[j]==p[i],但我更改之后,表示s[j-1]==p[i]。这有点不直观,而且AB连续的时候会带来一些问题,混在一起我就有点晕。
对于上图,我们主要观察SP在一起的情况:从0开始计数,S的第5、6、7列为1,P的第5、6、7列也为1。这是为什么?先看S的,S[5]=1是因为上面的i[5]=1,因为S可以匹配0长度的字符串,所以s(输入的待匹配字符串)的第5个“s”,应该让S之后的P也有资格匹配。若P匹配“s”,说明S匹配“0长度字符串”。体现出来就是上一行的1全落到了下一行。落下来的1之后的元素怎么判断值呢?直接看s[j-1]是不是和p[i]相等啊。比如说我们看S和它上面的i,i的1落下来,因此S[5]=1;然后由于s[5]==p[5],所以S[6]=1;同理S[7]=1。S[8]=0因为S和i不匹配。
这里一定要弄清楚。否则代码会有无数的边界条件错误。
第一行的初始化也是一样。
哇这两道题修修补补,真恶心死我了。
思路和我的相同,因此我就把我自己的优化一下,有两处改动:
class Solution {
bool DP[1050][1050];
public:
bool isMatch(string s, string p) {
if(!p.size()) return s.size() == 0;
if(p[0]=='*')
for(int j=0;j<=s.size();j++)
DP[0][j]=true;
else if(p[0]=='?'||p[0]==s[0])
DP[0][1]=1;
else
return false;
for(int i=1;i
最重要的:DP用bool不用int,时间缩短五倍以上!!!!然后就是把if判断放在=的右边,直接判断对错。
同时我们观察到,其实DP[i+1]仅仅与DP[i]有关,因此DP表仅需要两行即可,用不到那么多行,这个我就懒得写了。
同理第十题,我们仅将DP由int换为bool,就可以同样获得五倍的提升:
这样看来,DP表一定要是bool才好了。
10题还有一种优秀的DP,不需要像我这样先对p做处理,如下:
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.size(), n = p.size();
vector> dp(m + 1, vector(n + 1, false));
dp[0][0] = true;
for (int i = 0; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p[j - 1] == '*') {
dp[i][j] = dp[i][j - 2] || (i && dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.'));
} else {
dp[i][j] = i && dp[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == '.');
}
}
}
return dp[m][n];
}
};
就是如果前一个是*那么就看前一个的前一个,即使这个是*也没关系。
代码如下:
bool IsWildcardMatch(string s, string p)
{
int slen = s.size(), plen = p.size(), i, j, iStar = -1, jStar = -1;
for (i = 0, j = 0; i < slen; i++, j++)
{
if (j < plen && p[j] == '*')
{ //meet a new '*', update traceback i/j info
iStar = i;
jStar = j;
--i;
}
else
{
if (j >= plen || (p[j] != s[i] && p[j] != '?'))
{ // mismatch happens
if (iStar >= 0)
{ // met a '*' before, then do traceback
i = iStar++;
j = jStar;
}
else return false; // otherwise fail
}
}
}
while (j
具体思路就是将p以*作为分隔符分隔出多个子串,若是在s中找到了这些子串的对应,则p和s对应,举例如下:
missisibisp和m*ib*p:
m和m对应;之后遇到*,开始循环:先看ib和is,然后ib和ss、si、is、si、ib,ib和ib对应上了。为什么是ib?因为ib是由*分隔出的子串,找它的对应即可,然后看p和i、s、p,对应上了,退出循环。
又快又好的方法。
10题没法用这个方法。因为这个方法是基于*可以匹配无限的,所以才能用*来分隔子串,不用管*是否匹配成功,因为必定匹配成功。而10题中的*没有这么厉害,因此不能用。
10题的:
class Solution {
public:
bool isMatch(string s, string p) {
if (p.empty()) return s.empty();
if ('*' == p[1])
// x* matches empty string or at least one character: x* -> xx*
// *s is to ensure s is non-empty
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));
}
};
class Solution {
public:
bool isMatch(string s, string p) {
/**
* f[i][j]: if s[0..i-1] matches p[0..j-1]
* if p[j - 1] != '*'
* f[i][j] = f[i - 1][j - 1] && s[i - 1] == p[j - 1]
* if p[j - 1] == '*', denote p[j - 2] with x
* f[i][j] is true iff any of the following is true
* 1) "x*" repeats 0 time and matches empty: f[i][j - 2]
* 2) "x*" repeats >= 1 times and matches "x*x": s[i - 1] == x && f[i - 1][j]
* '.' matches any single character
*/
int m = s.size(), n = p.size();
vector> f(m + 1, vector(n + 1, false));
f[0][0] = true;
for (int i = 1; i <= m; i++)
f[i][0] = false;
// p[0.., j - 3, j - 2, j - 1] matches empty iff p[j - 1] is '*' and p[0..j - 3] matches empty
for (int j = 1; j <= n; j++)
f[0][j] = j > 1 && '*' == p[j - 1] && f[0][j - 2];
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
if (p[j - 1] != '*')
f[i][j] = f[i - 1][j - 1] && (s[i - 1] == p[j - 1] || '.' == p[j - 1]);
else
// p[0] cannot be '*' so no need to check "j > 1" here
f[i][j] = f[i][j - 2] || (s[i - 1] == p[j - 2] || '.' == p[j - 2]) && f[i - 1][j];
return f[m][n];
}
};
44题:
class Solution {
private:
bool helper(const string &s, const string &p, int si, int pi, int &recLevel)
{
int sSize = s.size(), pSize = p.size(), i, curLevel = recLevel;
bool first=true;
while(si=pSize) return true; // if the rest of p are all star, return true
for(i=si; icurLevel+1) return false; // if the currently processed star is not the last one, return
}
}
return false;
}
public:
bool isMatch(string s, string p) {
int recLevel = 0;
return helper(s, p, 0, 0, recLevel);
}
};
关键是剪枝。
注意DP的写法。以及一些常见的技巧。bool、int、判断什么的。以及减少空间复杂度的方法。