Leetcode10 正則表達式匹配

本題爲Leetcode上的困難題,給出了別人相應的題解,供自己和相關愛好者參考學習。

題目描述

請實現一個函數用來匹配包含'. '和'*'的正則表達式。模式中的字符'.'表示任意一個字符,而'*'表示它前面的字符可以出現任意次(含0次)。在本題中,匹配是指字符串的所有字符匹配整個模式。例如,字符串"aaa"與模式"a.a"和"ab*ac*a"匹配,但與"aa.a"和"ab*a"均不匹配。

示例 1:
輸入:
s = "aa"
p = "a"
輸出: false
解釋: "a" 無法匹配 "aa" 整個字符串。

示例 2:
輸入:
s = "aa"
p = "a*"
輸出: true
解釋: 因爲 '*' 代表可以匹配零個或多個前面的那一個元素, 在這裏前面的元素就是 'a'。因此,字符串 "aa" 可被視爲 'a' 重複了一次。

示例 3:
輸入:
s = "ab"
p = ".*"
輸出: true
解釋: ".*" 表示可匹配零個或多個('*')任意字符('.')。

示例 4:
輸入:
s = "aab"
p = "c*a*b"
輸出: true
解釋: 因爲 '*' 表示零個或多個,這裏 'c' 爲 0 個, 'a' 被重複一次。因此可以匹配字符串 "aab"。

示例 5:
輸入:
s = "mississippi"
p = "mis*is*p*."
輸出: false

/*方法1 遞歸回溯
遞推關係:
class Solution {
public:
    bool isMatch(string s, string p) {
        //方法1 遞歸回溯
        /*if(p.empty())   return s.empty();
        bool firstMatch = (!s.empty() && (s[0] == p[0] || p[0] == '.'));
        // 從p的第2個字符開始,如果爲 '*'
        if (p.size() >= 2 && p[1] == '*')       //第二個字符爲*
            //當第一個字符不匹配時,p串的前兩個字符可以被忽略
            //因此p需要分離前兩個字符進行遍歷,即isMatch(s, p.substr(2))
            //當第一個字符匹配時,表示p串的前兩個字符與s的第一個字符匹配
            //而p串又可以表示第一個字符出現了多次,需要重新與後續序列匹配
            //因此s需要分離第一個字符再進行遍歷,即firstMatch && isMatch(s.substr(1), p)
            return (isMatch(s, p.substr(2)) || (firstMatch && isMatch(s.substr(1), p)));
        else
            return firstMatch && isMatch(s.substr(1), p.substr(1));
    }
};

上述的遞歸代碼看似容易,實則難以理解,而本題還可以用動態規劃進行解決,以下是來自別人的題解思路:

1. 狀態

首先狀態dp一定能自己想出來,dp[i][j]表示s的前i個是否能夠被p的前j個匹配

2. 轉移方程

怎麼想轉移方程?首先想的時候從已經求出了 dp[i-1][j-1] 入手,再加上已知 s[i]、p[j],要想的問題就是怎麼去求 dp[i][j]。

已知 dp[i-1][j-1] 意思就是前面子串都匹配上了,不知道新的一位的情況。
那就分情況考慮,所以對於新的一位 p[j] s[i] 的值不同,要分情況討論:

(1)考慮最簡單的 p[j] == s[i] : dp[i][j] = dp[i-1][j-1],然後從 p[j] 可能的情況來考慮,讓 p[j]=各種能等於的東西。

(2)p[j] == "." : dp[i][j] = dp[i-1][j-1]

(3)p[j] ==" * ":

第一個難想出來的點:怎麼區分 ∗*∗ 的兩種討論情況

首先給了 *,明白 * 的含義是 匹配零個或多個前面的那一個元素,所以要考慮他前面的元素 p[j-1]。* 跟着他前一個字符走,前一個能匹配上 s[i],* 纔能有用,前一個都不能匹配上 s[i],* 也無能爲力,只能讓前一個字符消失,也就是匹配 000 次前一個字符。所以按照 p[j-1] 和 s[i] 是否相等,我們分爲兩種情況:

(1)p[j-1] != s[i] : dp[i][j] = dp[i][j-2]

這就是剛纔說的那種前一個字符匹配不上的情況。比如(ab, abc * )。遇到 * 往前看兩個,發現前面 s[i] 的 ab 對 p[j-2] 的 ab 能匹配,雖然後面是 c*,但是可以看做匹配 000 次 c,相當於直接去掉 c *,所以也是 True。注意 (ab, abc**) 是 False。

(2)p[j-1] == s[i] or p[j-1] == ".":

  • * 前面那個字符,能匹配 s[i],或者 * 前面那個字符是萬能的 .
  • 因爲 . * 就相當於 . .,那就只要看前面可不可以匹配就行。
  • 比如 (##b , ###b *),或者 ( ##b , ### . * ) 只看 ### 後面一定是能夠匹配上的。
  • 所以要看 b 和 b * 前面那部分 ## 的地方匹不匹配。

第二個難想出來的點:怎麼判斷前面是否匹配

  • dp[i][j] = dp[i-1][j] // 多個字符匹配的情況    
  • or dp[i][j] = dp[i][j-1] // 單個字符匹配的情況
  • or dp[i][j] = dp[i][j-2] // 沒有匹配的情況    

看 ### 匹不匹配,不是直接只看 ### 匹不匹配,要綜合後面的 b b* 來分析。這三種情況是 ororor 的關係,滿足任意一種都可以匹配上,同時是最難以理解的地方:

dp[i-1][j] 就是看 s 裏 b 多不多, ### 和 ###b * 是否匹配,一旦匹配,s 後面再添個 b 也不影響,因爲有 * 在,也就是 ###b 和 ###b *也會匹配。

dp[i][j-1] 就是去掉 * 的那部分,###b 和 ###b 是否匹配,比如 qqb qqb

dp[i][j-2] 就是 去掉多餘的 b *,p 本身之前的能否匹配,###b 和 ### 是否匹配,比如 qqb qqbb* 之前的 qqb qqb 就可以匹配,那多了的 b * 也無所謂,因爲 b * 可以是匹配 000 次 b,相當於 b * 可以直接去掉了。

三種滿足一種就能匹配上。爲什麼沒有 dp[i-1][j-2] 的情況? 就是 ### 和 ### 是否匹配?因爲這種情況已經是 dp[i][j-1] 的子問題。也就是 s[i]==p[j-1],則 dp[i-1][j-2]=dp[i][j-1]。

總結

        如果 p.charAt(j) == s.charAt(i) : dp[i][j] = dp[i-1][j-1];
        如果 p.charAt(j) == '.' : dp[i][j] = dp[i-1][j-1];
        如果 p.charAt(j) == '*':

    如果 p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2] //in this case, a* only counts as empty
           如果 p.charAt(i-1) == s.charAt(i) or p.charAt(i-1) == '.':
                   dp[i][j] = dp[i-1][j] //in this case, a* counts as multiple a
                   or dp[i][j] = dp[i][j-1] // in this case, a* counts as single a
                   or dp[i][j] = dp[i][j-2] // in this case, a* counts as empty

class Solution {
public:
    bool isMatch(string s, string p) {
        if (p.empty())  return s.empty();
        // 前面加某一相同字符,
        // 防止 (ab, c*ab) 這樣的匹配,
        // 避免複雜的初始化操作
        s = " " + s;
        p = " " + p;
        int m = s.size(), n = p.size();
        // 定義記憶數組,並初始化爲false
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
        // 設添加的字符爲真
        dp[0][0] = true;
        // 記憶數組能保持字符串上一個字符的狀態
        // 因此可以對下一個字符進行判斷
        for (int i = 1; i < m + 1; i++) {
            for (int j = 1; j < n + 1; j++) {
                // 不帶 '*' 號時的匹配
                if (s[i - 1] == p[j - 1] || p[j - 1] == '.') 
                    dp[i][j] = dp[i - 1][j - 1];
                else if (p[j - 1] == '*') {
                    // 考慮 '*' 時的兩種情況
                    if (s[i - 1] != p[j - 2] && p[j - 2] != '.')
                        dp[i][j] = dp[i][j - 2];
                    else
                    dp[i][j] = dp[i][j - 2] || dp[i - 1][j];
                }
            }
        }
        // 返回最後字符的匹配狀態
        return dp[m][n];
    }
};

 

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