动态规划之正则表达
動態規劃之正則表達
給你一個字符串 s 和一個字符規律 p,請你來實現一個支持 ‘.’ 和 ‘*’ 的正則表達式匹配。
- ‘.’ 匹配任意單個字符
- ‘*’ 匹配零個或多個前面的那一個元素
所謂匹配,是要涵蓋 整個 字符串 s的,而不是部分字符串。
說明:
s 可能為空,且只包含從 a-z 的小寫字母。 p 可能為空,且只包含從 a-z 的小寫字母,以及字符 . 和 *。示例 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再次解決這道題!!!!!
方法一:DP table
在進行下面的處理之前,需要明確動態規劃的幾個步驟:
狀態
首先狀態 dp 一定能自己想出來。
dp[i][j] 表示 s 的前 i 個是否能被 p 的前 j 個匹配
轉移方程
怎么想轉移方程?首先想的時候從已經求出了 dp[i-1][j-1] 入手,再加上已知 s[i]、p[j],要想的問題就是怎么去求 dp[i][j]。
已知 dp[i-1][j-1] 意思就是前面子串都匹配上了,不知道新的一位的情況。
那就分情況考慮,所以對于新的一位 p[j] s[i] 的值不同,要分情況討論:
考慮最簡單的 p[j] == s[i] : dp[i][j] = dp[i-1][j-1]
然后從 p[j] 可能的情況來考慮,讓 p[j]=各種能等于的東西。
p[j] == "." : dp[i][j] = dp[i-1][j-1]
p[j] ==" * ":
前兩種情況比較容易解決,接下來就對幾種情況分別進行討論:
處理「*」 通配符
第一個難想出來的點:怎么區分 ‘*’ 的兩種討論情況
首先給了 ‘*’,明白 ‘*’ 的含義是 匹配零個或多個前面的那一個元素,所以要考慮他前面的元素 p[j-1]。‘*’ 跟著他前一個字符走,前一個能匹配上 s[i],‘*’ 才能有用,前一個都不能匹配上 s[i],‘*’ 也無能為力,只能讓前一個字符消失,也就是匹配 0 次前一個字符
所以按照 p[j-1] 和 s[i] 是否相等,我們分為兩種情況:
這就是剛才說的那種前一個字符匹配不上的情況。
比如(ab, abc * )。遇到 ‘ * ’ 往前看兩個,發現前面 s[i] 的 ab 對 p[j-2] 的 ab 能匹配,雖然后面是 ‘c*’,但是可以看做匹配 0 次 c,相當于直接去掉 ’c * ‘,所以也是 true。注意 (ab, abc**) 是 False。
' * ' 前面那個字符,能匹配 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* 來分析這三種情況是 or 的關系,滿足任意一種都可以匹配上,同時是最難以理解的地方:
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 * 可以是匹配 00 次 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]如果 p.charAt(i-1) == s.charAt(i) or p.charAt(i-1) == '.':dp[i][j] = dp[i-1][j] or dp[i][j] = dp[i][j-1] or dp[i][j] = dp[i][j-2]完整代碼:
public boolean isMatch(String s,String p){if (s == null || p == null) {return false;}boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];dp[0][0] = true;//dp[i][j] 表示 s 的前 i 個是否能被 p 的前 j 個匹配for (int i = 0; i < p.length(); i++) { // here's the p's length, not s'sif (p.charAt(i) == '*' && dp[0][i - 1]) {dp[0][i + 1] = true; // here's y axis should be i+1}}for (int i = 0; i < s.length(); i++) {for (int j = 0; j < p.length(); j++) {if (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i)) {//如果是任意元素 或者是對于元素匹配dp[i + 1][j + 1] = dp[i][j];}if (p.charAt(j) == '*') {if (p.charAt(j - 1) != s.charAt(i) && p.charAt(j - 1) != '.') {//如果前一個元素不匹配 且不為任意元素dp[i + 1][j + 1] = dp[i + 1][j - 1];} else {dp[i + 1][j + 1] = (dp[i + 1][j] || dp[i][j + 1] || dp[i + 1][j - 1]);/*dp[i][j] = dp[i-1][j] // 多個字符匹配的情況 or dp[i][j] = dp[i][j-1] // 單個字符匹配的情況or dp[i][j] = dp[i][j-2] // 沒有匹配的情況*/}}}}return dp[s.length()][p.length()];}C++代碼:
class Solution { public:// 動態規劃bool isMatch(string s, string p) {int ns = s.size();int np = p.size();if(p.empty()) //注意這里判斷的寫法,不能是if(p.empty() || s.empty());原因試一下就知道啦return s.empty();vector<vector<bool>> dp(ns+1, vector<bool>(np+1, false));//就是博客中的問題,怎么判斷前面是否匹配,相當于提前把遇到‘*’的情況在dp[i][j - 2]//的情況先記錄下來,后面使用,因為初始化的時候全部都是false,所以要提前處理dp[0][0] = true;for(int i = 1; i <= np; i++){if(i-2 >= 0 && p[i-1] == '*' && p[i-2]){dp[0][i] = dp[0][i-2];}}for(int i = 1; i <= ns; i++){for(int j = 1; j <= np; j++){if(p[j-1] == s[i-1] || p[j-1] == '.')dp[i][j] = dp[i-1][j-1];//萬金油,直接相等if(p[j-1] == '*'){bool zero, one;if(j-2 >= 0){zero = dp[i][j-2];//匹配0次,one = (p[j-2] == s[i-1] || p[j-2] == '.') && dp[i-1][j];//匹配1次dp[i][j] = zero || one;//有一個為真即可}}}}return dp[ns][np];}};方法二: 動態規劃
第?步, 我們暫時不管正則符號, 如果是兩個普通的字符串進??較, 如何
進?匹配? 我想這個算法應該誰都會寫:
然后, 稍微改造?下上?的代碼, 略微復雜了?點, 但意思還是?樣的,很容易理解吧:
bool isMatch(string text, string pattern) {int i = 0; // text 的索引位置int j = 0; // pattern 的索引位置while (j < pattern.size()) {if (i >= text.size())return false;if (pattern[j++] != text[i++])return false;} // 相等則說明完成匹配return j == text.size(); }如上改寫, 是為了將這個算法改造成遞歸算法(偽碼) :
def isMatch(text, pattern) -> bool:if pattern is empty: return (text is empty?)first_match = (text not empty) and pattern[0] == text[0]return first_match and isMatch(text[1:], pattern[1:])處理點號「.」 通配符
點號可以匹配任意?個字符, 萬?油嘛, 其實是最簡單的, 稍加改造即可:
def isMatch(text, pattern) -> bool:if not pattern: return not textfirst_match = bool(text) and pattern[0] in {text[0], '.'}return first_match and isMatch(text[1:], pattern[1:])處理「*」 通配符
星號通配符可以讓前?個字符重復任意次數, 包括零次。 那到底是重復?次
呢? 這似乎有點困難, 不過不要著急, 我們起碼可以把框架的搭建再進?
步:
星號前?的那個字符到底要重復?次呢? 這需要計算機暴?窮舉來算, 假設
重復 N 次吧。 前?多次強調過, 寫遞歸的技巧是管好當下, 之后的事拋給遞歸。 具體到這?, 不管 N 是多少, 當前的選擇只有兩個: 匹配 0 次、 匹
配 1 次。 所以可以這樣處理:
可以看到, 我們是通過保留 pattern 中的「*」 , 同時向后推移 text, 來實現
「」 將字符重復匹配多次的功能。 舉個簡單的例?就能理解這個邏輯了。 假
設 pattern = a , text = aaa, 畫個圖看看匹配過程:
選擇使?「備忘錄」 遞歸的?法來降低復雜度
我將暴?解法和優化解法放在?起, ?便你對?,
# 帶備忘錄的遞歸 def isMatch(text, pattern) -> bool:memo = dict() # 備忘錄def dp(i, j):if (i, j) in memo: return memo[(i, j)]if j == len(pattern): return i == len(text)first = i < len(text) and pattern[j] in {text[i], '.'}if j <= len(pattern) - 2 and pattern[j + 1] == '*':ans = dp(i, j + 2) or \first and dp(i + 1, j)else:ans = first and dp(i + 1, j + 1)memo[(i, j)] = ansreturn ansreturn dp(0, 0)# 暴?遞歸 def isMatch(text, pattern) -> bool:if not pattern: return not textfirst = bool(text) and pattern[0] in {text[0], '.'}if len(pattern) >= 2 and pattern[1] == '*':return isMatch(text, pattern[2:]) or \first and isMatch(text[1:], pattern)else:return first and isMatch(text[1:], pattern[1:])總結
- 上一篇: 动态规划之四键键盘
- 下一篇: LeetCode中二叉树相关题