44. Wildcard Matching 通配符匹配

Title

給定一個字符串 (s) 和一個字符模式 § ,實現一個支持 ‘?’ 和 ‘*’ 的通配符匹配。

'?' 可以匹配任何單個字符。
'*' 可以匹配任意字符串(包括空字符串)。

兩個字符串完全匹配纔算匹配成功。

說明:

s 可能爲空,且只包含從 a-z 的小寫字母。
p 可能爲空,且只包含從 a-z 的小寫字母,以及字符 ? 和 *。

示例 1:

輸入:

s = "aa"
p = "a"

輸出: false
解釋: “a” 無法匹配 “aa” 整個字符串。

示例 2:

輸入:

s = "aa"
p = "*"

輸出: true
解釋: ‘*’ 可以匹配任意字符串。

示例 3:

輸入:

s = "cb"
p = "?a"

輸出: false
解釋: ‘?’ 可以匹配 ‘c’, 但第二個 ‘a’ 無法匹配 ‘b’。

示例 4:

輸入:

s = "adceb"
p = "*a*b"

輸出: true
解釋: 第一個 ‘’ 可以匹配空字符串, 第二個 '’ 可以匹配字符串 “dce”.

示例 5:

輸入:

s = "acdcb"
p = "a*c?b"

輸出: false

動態規劃

Solve

在給定的模式 p 中,只會有三種類型的字符出現:

  • 小寫字母 a−z,可以匹配對應的一個小寫字母;
  • 問號 ?,可以匹配任意一個小寫字母;
  • 星號 *,可以匹配任意字符串,可以爲空,也就是匹配零或任意多個小寫字母。

其中「小寫字母」和「問號」的匹配是確定的,而「星號」的匹配是不確定的,因此我們需要枚舉所有的匹配情況。爲了減少重複枚舉,我們可以使用動態規劃來解決本題。

我們用 dp[i][j] 表示字符串 s 的前 i 個字符和模式 p 的前 j 個字符是否能匹配。

在進行狀態轉移時,我們可以考慮模式 p 的第 j 個字符 pj,與之對應的是字符串 s 中的第 i 個字符 si

  • 如果 pj 是小寫字母,那麼 si 必須也爲相同的小寫字母,狀態轉移方程爲:dp[i][j]=(si 與 pj 相同)∧dp[i−1][j−1]
  • 如果 pj 是問號,那麼對 si 沒有任何要求,狀態轉移方程爲:
  • 如果 pj 是星號,那麼同樣對 si 沒有任何要求,但是星號可以匹配零或任意多個小寫字母,因此狀態轉移方程分爲兩種情況,即使用或不使用這個星號:dp[i][j]=dp[i][j−1]∨dp[i−1][j]

最終的狀態轉移方程如下:

dp[i][j]={(sipj)dp[i1][j1],pjdp[i1][j1],pjdp[i][j1]dp[i1][j],pj dp[i][j]= \begin{cases} (s_i與p_j相同)∧dp[i−1][j−1], & p_j 是小寫字母 \\ dp[i−1][j−1], & p_j 是問號 \\ dp[i][j−1]∨dp[i−1][j], & p_j 是星號 \end{cases}

我們也可以將前兩種轉移進行歸納:
dp[i][j]={dp[i1][j1],sipjpjdp[i][j1]dp[i1][j],pjFalse, dp[i][j]= \begin{cases} dp[i−1][j−1], & s_i與p_j相同或者p_j 是問號 \\ dp[i][j−1]∨dp[i−1][j], & p_j 是星號 \\ False, & 其它情況 \end{cases}

細節

只有確定了邊界條件,才能進行動態規劃。在上述的狀態轉移方程中,由於 dp[i][j] 對應着 s 的前 i 個字符和模式 p 的前 j 個字符,因此所有的 dp[0][j] 和 dp[i][0] 都是邊界條件,因爲它們涉及到空字符串或者空模式的情況,這是我們在狀態轉移方程中沒有考慮到的:

  • dp[0][0]=True,即當字符串 s 和模式 p 均爲空時,匹配成功;

  • dp[i][0]=False,即空模式無法匹配非空字符串;

  • dp[0][j] 需要分情況討論:因爲星號才能匹配空字符串,所以只有當模式 p 的前 j 個字符均爲星號時,dp[0][j] 才爲真。

我們可以發現,dp[i][0] 的值恆爲假,dp[0][j] 在 j 大於模式 p 的開頭出現的星號字符個數之後,值也恆爲假,而 dp[i][j] 的默認值(其它情況)也爲假,因此在對動態規劃的數組初始化時,我們就可以將所有的狀態初始化爲False,減少狀態轉移的代碼編寫難度。

最終的答案即爲 dp[m][n],其中 m 和 n 分別是字符串 s 和模式 p 的長度。需要注意的是,由於大部分語言中字符串的下標從 0 開始,因此 si 和 pj 分別對應着 s[i−1] 和 p[j−1]。

Code

	def isMatch_dp(self, s: str, p: str) -> bool:
		lengthS, lengthP = len(s), len(p)
		dp = [[False] * (lengthP + 1) for _ in range(lengthS + 1)]
		dp[0][0] = True
		for i in range(1, lengthP + 1):
			if p[i - 1] == '*':
				dp[0][i] = True
			else:
				break
		for i in range(1, lengthS + 1):
			for j in range(1, lengthP + 1):
				if p[j - 1] == '*':
					dp[i][j] = dp[i][j - 1] or dp[i - 1][j]
				elif p[j - 1] == '?' or s[i - 1] == p[j - 1]:
					dp[i][j] = dp[i - 1][j - 1]
		return dp[lengthS][lengthP]

複雜度分析

時間複雜度:O(mn),其中 m 和 n 分別是字符串 s 和模式 p 的長度。

空間複雜度:O(mn),即爲存儲所有 (m+1)(n+1) 個狀態需要的空間。此外,在狀態轉移方程中,由於 dp[i][j] 只會從 dp[i][…] 以及 dp[i−1][…] 轉移而來,因此我們可以使用滾動數組對空間進行優化,即用兩個長度爲 n+1 的一維數組代替整個二維數組進行狀態轉移,空間複雜度爲 O(n)。

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