對於p字符串有點、字母、點*、字母*四種元素,點匹配任意一個字母,字母匹配相同的一個字母,點*匹配任意字母(可以是任意不同字母,例如.*匹配abc),字母*匹配連續任意個相同字母,值得注意的是*的任意包括0個。由於*可以匹配任意個,造成檢驗s和p是否完全匹配的時候難以確定究竟*匹配幾個字母合適,這正是本題的關鍵點。題意簡單粗暴,看一下原題,然後分析一下如何處理。
Given an input string (
s
) and a pattern (p
), implement regular expression matching with support for'.'
and'*'
.'.' Matches any single character. '*' Matches zero or more of the preceding element.
The matching should cover the entire input string (not partial).
Note:
s
could be empty and contains only lowercase lettersa-z
.p
could be empty and contains only lowercase lettersa-z
, and characters like.
or*
.Example 1:
Input: s = "aa" p = "a" Output: false Explanation: "a" does not match the entire string "aa".
Example 2:
Input: s = "aa" p = "a*" Output: true Explanation: '*' means zero or more of the precedeng element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".
Example 3:
Input: s = "ab" p = ".*" Output: true Explanation: ".*" means "zero or more (*) of any character (.)".
Example 4:
Input: s = "aab" p = "c*a*b" Output: true Explanation: c can be repeated 0 times, a can be repeated 1 time. Therefore it matches "aab".
Example 5:
Input: s = "mississippi" p = "mis*is*p*." Output: false
一、動態規劃
開始前的準備工作
當p中的a*遇到s中的aaa的時候,由於事先不知道那哪種匹配可能是正確的或者有幾種是正確的,應該嘗試匹配空、a、aa、aaa等全部可能的情況,而動態規劃實際上就是一種窮舉所所有情況的好方法。假設s有m個元素,p有n個元素,這裏我選擇建立dp[m+1][n+1]數組,之所以多1排1列是爲了處理首排首列的時候方便一些,減少特殊情況的單獨列出。橫排m+1代表空白一個首元素+s中的m個元素,豎排n+1代表空白一個元素+p中的n個元素,相當於給s和p開頭加了一個可有可無的空白元素。先處理匹配s前面加的空白元素匹配問題,p可以使用任意空白去匹配(加的空白元素和.*和字母*等),如果匹配,對應位爲true。
然後重點處理s中的m個元素匹配
每一次匹配結果受三個方向(dp二維數組)影響,例如aab和c*a*b,假如當前在匹配前者的b和後者的a。
1、上方向代表s當前元素和p上一個元素的匹配情況(即b和*),如果當前p元素是*,而*的前一個元素匹配了s的當前元素,則加上*也一定匹配,這種匹配方式等於用字母*或.*只匹配一個元素。上方向也可以代表s當前元素和p上兩個元素匹配情況(即a和c是否匹配),這種匹配方式等於把字母*或.*一併拿出來,匹配0個元素,如果匹配,則加上字母*或.*這個空白也一定匹配。如果當前p元素不是*不用參考此方向。
2、左方向代表s上一個元素和p當前元素的匹配情況(即a和b),如果當前p元素是*,而字母*或.*已經匹配s的上一個元素,又能匹配s的當前元素,就可以把字母*或.*的匹配數量延長一個,到達2或更多。至此就包括了字母*或.*匹配0個、1個或多個元素的全部情況。
3、左上方向代表s上一個元素和p上一個元素的匹配情況(即a和*),如果當前元素是字母或者.,也就是說不是*,則若此處匹配,必須是p的上一個元素匹配s的上一個元素且p的當前元素也匹配s的當前元素。
檢查一遍已經包括了所有的情況,沒有遺漏。其中1和2處理*的匹配,這是最複雜的情況,3處理字母和.的匹配,比較容易。
對照一下代碼和註釋,可以把代碼和以上三種方向對照一下。
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 j = 1;j < n && p[j] == '*';j+=2)
dp[0][j+1] = true;
for (int i = 0;i < m;i++)
for (int j = 0;j < n;j++){
if (p[j]!='*')
//如果是字母或.只受左上方向影響,即方向3
dp[i+1][j+1] = p[j]=='.' ? dp[i][j] : (dp[i][j] && (p[j]==s[i]));
else
//如果是*則受左和上兩個方向影響,即方向1和2,有三個或分別對應上述三種情況
dp[i+1][j+1] = dp[i+1][j-1] || dp[i+1][j] || (dp[i][j+1]&&((p[j-1]==s[i])||p[j-1]=='.'));
}
return dp[m][n];
}
};
二、遞歸
對於*可以匹配任意數量元素的不確定性也可以用遞歸處理,可不可行試過才知道,通過深入下一層遞歸判斷。所以有三大類情況:匹配、不匹配、不知道需要深入判斷。其中細分情況比較多,我放在代碼註釋裏一一解釋。記住一點,只有匹配到s和p都沒有剩餘元素了纔算成功。
bool match(string& s,int i,string& p,int j){
if (i==s.size() && j==p.size()) //如果s和p都沒有剩餘元素了,則匹配成功
return true;
if (j==p.size()) //如果s還有元素但p沒了,則匹配失敗
return false;
if (j+1<p.size() && p[j+1]=='*'){ //如果p的下一個元素是*,當作整體處理,分爲.*和字母*討論
if(p[j]=='.'){ //如果是.*
for (int pos = i;pos <= s.size();pos++) //窮舉匹配0、1、多個元素的情況,有一個匹配就成功,都失敗則徹底失敗
if (match(s,pos,p,j+2)) //注意這裏不能return match(),否則窮舉不了後面的情況
return true;
return false;
}
else{ //如果是字母*
if(match(s,i,p,j+2)) return true; //匹配0個元素,等於跳過字母*,同樣不能return match()
for (int pos = i;pos<s.size()&&s[pos]==p[j];pos++) //匹配1、多個元素,比.*多考慮的是隻有當前的一個匹配上了,才能考慮匹配更多的元素
if (match(s,pos+1,p,j+2))
return true;
return false;
}
}
else{ //如果是字母或.,匹配上了的話深入下一層看能否成功,此處終於可以return match()了
if (i<s.size() && (s[i]==p[j] || p[j]=='.'))
return match(s,i+1,p,j+1);
else
return false;
}
}
class Solution {
public:
bool isMatch(string s, string p) {
return match(s,0,p,0);
}
};
個人認爲動態規劃代碼簡潔漂亮,內涵卻不簡單,兩句話寫盡人世鉛華,遞歸寫的比較粗糙,又臭又長,勝在情況分類比較清晰。最後歡迎大家留言討論,如有錯誤或改進還請不吝賜教。