總有一天會用到的筆記

本站為減緩筆者下列疑難雜症誕生:記性很差,學過就忘;智商低落,囫圇吞棗。

0%

【模板】KMP

KMP 用在字串匹配,可以在字串 \(S\) 中找到哪些地方出現過字串 \(P\)

核心思想是優化樸素的字串匹配過程。匹配可以視作,每個位移過後的 \(P\),有哪些與對應的 \(S\) 子字串相等:

1
2
3
4
5
6
S  AABAABAAC
P1 AABAAc
P2 Aabaac
P3 aabaac
P4 AABAAC
每個都匹配到失敗為止,並且從匹配失敗之後用小寫表示
  • 在匹配過程中,想辦法跳過會更早失敗的 \(P\)

在匹配過程中,有時會失敗在某個地方,如在第 1 次匹配中 \(S[4] \ne P[4]\)。 第 2和第 3 次匹配,會在更早的地方失敗。我們想直接跳過 2、3 次匹配。 要找到下一個至少不會在 \(i<4\) 就匹配失敗的。 可以發現,這相當於把 \(P\) 字串位移直到其次長前綴 = 匹配失敗前的後綴(最長就根本沒位移):

1
2
P1 AAB[AA]c
P4 [AA]BAAC
  • 訂定函數 \(f(i)\),代表 \(P[0,i)\) 次長共同前後綴長度(最長但不包括 \(P[0,i)\) 本身)

上面的想法,相當於以 \(i\) \(j\) 兩個指針來匹配 \(S\)\(P\),一但失敗就使 \(j\) 回到最次長前綴。 定義 \(f(i)\) 可以方便地做到這件事。

  • 轉移:\(f(i+1) = f(j)+1,\ P[j] = P[i]\)
image-20211109190643981

我們來計算 \(f(i)\)。只要 \(P[f(i)] = P[i]\),則 \(f(i+1) = f(i)+1\) 那如果不相等呢?也還有機會:

image-20211109192615200

因為 \(P[0, f(i)) = P[i-f(i),i)\),所以 \(P[0,f(f(i))) = P[i-f(f(i),i)\)。 此時只要從 \(f(i)\) 退到 \(f(f(i))\),再試試看就好了。

  • 基底:\(f(0) = -1,\ f(1) = 0\)

顯然,\(f(1) = 0\)。那 \(f(0)\) 呢? 在匹配過程中會出現 \(f(0)\),即是當 \(S[i]\)\(P[0]\) 匹配,而且還失敗的時候。 這時就直接放棄這個 \(S[i]\),從 \(S[i+1]\)\(P[0]\) 開始重新匹配了。 我們令 \(f(0) = -1\),方便我們知道 \(S[i]\)\(P[0]\) 匹配失敗,該直接跳過了。實作時會體現 \(-1\) 的方便性。

以下是 \(f\) 建表的實作:

1
2
3
4
5
6
f[0]=-1, f[1]=0;
for (int i=1,j; i<p.size(); i++)
{
for (j=f[i]; j>=0 && p[j]!=p[i]; j=f[j]);
f[i+1] = j+1;
}
  • \(i\) 遍歷 \(S\)\(j\) 遍歷 \(P\),對於每個 \(i\) 找到第一個與其匹配的 \(j\)

匹配 \(i\)\(j\) 一但成功,就能幫下個 \(i\) 找匹配的 \(j\)。 否則,就找到下一個可能與 \(i\) 匹配的 \(P\)。 另外,當 \(i\)\(j=0\) 匹配失敗,就 \(i++,j=0\)

出現完全成功配對的條件,是過程中發生,\(i=|S|-1\)\(j=|P|-1\) 匹配成功。 下層迴圈中 \(j = |P|\),必須使 \(j = f[j]\)。實作用到了一個細節是,p[p.size()] 會拿到 p.end(),所以不用擔心 RE。ㄦ

1
2
3
4
5
for (int i=0,j=0; i<s.size(); i++,j++)
{
while (j>=0 && s[i]!=p[j]) j = f[j];
if (j == p.size()-1) ans++;
}