2020年4月14日 星期二

Leetcode題解 Python:Regular Expression Matching

這裡的表達式的特殊符號只有 ".", "*" 所以蠻簡化的。

想到正則表達式,如果能夠 import re,那絕對相當地快速。用 re 的函式就不需要去寫判斷了。

今天拋棄 re,這題限制居多,輸入字串不能局部配對,一定要全部吻合。難度會比真的re運作還簡單不少。

在我心目中,不能只是解題,還要符合 re 的運作模式,才是我要的解法。

但還是先解題優先吧。

首先有 si 跟 pi,代表兩個字串的索引,兩者都從 0 找起,如果 si 到底(等於s長度),而且也 pi 到底,代表 s 跟 p 全部匹配。

說到特殊符號,"."相對容易處理,這裡就不提了,不過"*"就困難一些,"*"代表前面字符可以出現 0或1次或1次以上,因此產生了許多可能性。

由於"*"僅影響前面的字符,所以當我們在匹配 s[si] 與 p[pi] 時,要考慮 p[pi+1]是否為"*",同時也要小心是否 pi+1 超出邊界,以免p[pi+1]報錯。

當後面是"*"時,匹配有三種變化,分別對應 0 或 1次 或 1次以上 的情形
第一種出現0次,si保持不到,而pi直接+2跳過"*",繼續遞迴下去。
第二種出現1次,若有匹配到,si+1 pi+1,往深遞迴。
第三種出現多次,若有匹配到,si+1 pi不變,往深遞迴。

由於"*"使得可能性開枝散葉變多,而造成重複計算,而且每組 si pi都只會有一種結果,因此可以使用dp去減少計算時間。(這裡使用lru_cache)

這讓我更了解dp的使用時機:會重複計算,而且相同參數只有會一種結果。

接下來要考慮例外,如果 (s,p) = ("","")、("",".*")、("a","")、("","b"),這些情況下能順利解決?

這裡先看到 si == len(s),搜尋到了底,這時要提防 p 是否還有長度可以搜尋,如果剩下".*"要如何解決。

當 si == len(s),代表只剩 "" 可以拿來比較,如果剩餘的p可以匹配""字串,那代表兩者相同。

而p能匹配空字串的,只有p[pi]後面是"*"。

空字串只會匹配後面有"*"的第一種情形,於是pi+2。如果剛好是".*"也能匹配到,但匹配到的結果會使 si +1 而往下會使得 si > len(s),超出範圍就不用匹配。

這樣子寫,也不需要在開頭做例外處理。
class Solution:
    def isMatch(self, s: str, p: str) -> bool:   
        from functools import lru_cache
        n, n2 = len(s), len(p)
        self.res = False       
        
        @lru_cache(None)
        def nextMatch(si, pi):
            if si > n or pi >= n2:
                if si == n and pi == n2: self.res = True
                return False
            elif si == n:
                c = ""
            else:
                c = s[si]
            #
            ch = p[pi]
            # 如果後面是 "*" 字,分別採取不同搜尋
            if pi+1 < n2 and p[pi+1] == "*":                 
                if c == ch or ch == ".":
                    nextMatch(si+1, pi) # 原地不動
                    nextMatch(si+1, pi+2) # 或向前走      
                nextMatch(si, pi+2) # 當作沒有配對到
            else: 
                # 後面沒有 * 
                if c == ch or ch == ".":
                    nextMatch(si+1, pi+1)

        nextMatch(0, 0)
        return self.res