本題爲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];
}
};