一、概述
輸入兩個字符串:s和p,判斷s能否匹配p。
這倆題要求差不多,44題最開始我沒用DP,一點一點循環匹配,結果最多過了1600+testcase,剩下的過不去,因爲循環匹配無法覆蓋所有的邊界條件。當時很難受,因爲邊界條件有很多,我一個一個測出來然後補救,最終發現有一類邊界條件無法補救,給我這思路判了死刑。但是沒法子。就擱置在一邊。現在開始學DP,就把這道題拎出來又做了一遍。沒有看Solution,直接自己分析做的。時空複雜度我自己都看樂了。太差了:
這是10題:
這是44題:
簡直是我AC的題目裏面時空複雜度最差的。但好歹是自己想出來了,還是寫下來吧。
二、分析
1、44題我的思路
對這倆題來說,我的思路是類似的。因此以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<p.size();++i)
{
for(int j=0;j<=s.size();++j)
{
if(p[i]=='*'&&DP[i-1][j]==1)
{
while(j<=s.size())
{
DP[i][j]=1;
j++;
}
break;
}
else if(p[i]=='?')
DP[i][j+1]=(DP[i-1][j]==1);
else
DP[i][j+1]=((DP[i-1][j]==1)&&(s[j]==p[i]));
}
}
if(DP[p.size()-1][s.size()]==1)
return true;
return false;
}
};
注意以下幾點:
第一,遞推的初始條件,也就是DP的第一行是要我們自己提前寫出來的:分情況填入不同個數個1即可;
第二,DP[i-1][j-1]不好實現,對於j=0的情況不友好,因此我將遞推條件改爲
DP[i][j+1]=(DP[i-1][j]==1)&&(p[i]==s[j])了。對應的判斷成功匹配也要重新改一下。
我這種時空複雜度都是O(mn),可以說是很差了。
2、10題我的思路
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<p.size();i++)
{
if(new_p.size()==0)
{
new_p+=p[i];
continue;
}
if(p[i]=='*')
{
if(new_p[new_p.size()-1]=='*')
continue;
else
new_p+=p[i];
}
else
new_p+=p[i];
}
cout<<new_p<<'\n';
string last_p="";
for(int i=0;i<new_p.size()-1;i++)
{
if(new_p[i+1]!='*')
last_p+=new_p[i];
else
{
if(new_p[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<<last_p<<'\n';
if(last_p[0]=='.')
DP[0][1]=1;
else if(last_p[0]=='?')
{
for(int i=0;i<=s.size();i++)
DP[0][i]=1;
}
else if(last_p[0]>='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<last_p.size();++i)
{
for(int j=0;j<=s.size();++j)
{
if(last_p[i]=='?'&&DP[i-1][j]==1)
{
while(j<=s.size())
{
DP[i][j]=1;
j++;
}
break;
}
else if(last_p[i]=='.')
DP[i][j+1]=(DP[i-1][j]==1);
else if(last_p[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<last_p.size();i++)
{
for(int j=0;j<=s.size();j++)
cout<<DP[i][j];
cout<<'\n';
}
if(DP[last_p.size()-1][s.size()]==1)
return true;
return false;
}
};
太醜陋了,真是太醜陋了,我這道題應該是迄今爲止我自己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不匹配。
這裏一定要弄清楚。否則代碼會有無數的邊界條件錯誤。
第一行的初始化也是一樣。
哇這兩道題修修補補,真噁心死我了。
3、44題、10題優秀的DP
思路和我的相同,因此我就把我自己的優化一下,有兩處改動:
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<p.size();++i)
{
DP[i][0]=p[i]=='*'&&DP[i-1][0];
for(int j=1;j<=s.size();++j)
{
if(p[i]=='*')
DP[i][j]=(DP[i][j-1]||DP[i-1][j]);
else
DP[i][j]=(DP[i-1][j-1]&&((p[i]=='?')||(s[j-1]==p[i])));
}
}
if(DP[p.size()-1][s.size()])
return true;
return false;
}
};
最重要的: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<vector<bool>> dp(m + 1, vector<bool>(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];
}
};
就是如果前一個是*那麼就看前一個的前一個,即使這個是*也沒關係。
4、44題優秀的指針法
代碼如下:
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<plen && p[j] == '*') ++j;
return j == plen;
}
具體思路就是將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題中的*沒有這麼厲害,因此不能用。
5、10題和44題優秀的遞歸方法
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<vector<bool>> f(m + 1, vector<bool>(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<sSize && (p[pi]==s[si] || p[pi]=='?')) {++pi; ++si;} //match as many as possible
if(pi == pSize) return si == sSize; // if p reaches the end, return
if(p[pi]=='*')
{ // if a star is met
while(p[++pi]=='*'); //skip all the following stars
if(pi>=pSize) return true; // if the rest of p are all star, return true
for(i=si; i<sSize; ++i)
{ // then do recursion
if(p[pi]!= '?' && p[pi]!=s[i]) continue;
if(first) {++recLevel; first = false;}
if(helper(s, p, i, pi, recLevel)) return true;
if(recLevel>curLevel+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、判斷什麼的。以及減少空間複雜度的方法。