【LeetCode】10. Regular Expression Matching【動態規劃&遞歸】

       對於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 letters a-z.
  • p could be empty and contains only lowercase letters a-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);
    }
};

個人認爲動態規劃代碼簡潔漂亮,內涵卻不簡單,兩句話寫盡人世鉛華,遞歸寫的比較粗糙,又臭又長,勝在情況分類比較清晰。最後歡迎大家留言討論,如有錯誤或改進還請不吝賜教。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章