Algorithm
Task
給定一個文本串 \(S\) 和一個模式串 \(T\),求 \(T\) 在 \(S\) 中出現的所有位置。
Limitations
要求時空複雜度均爲線性。
Solution
回頭重新學一遍看毛片 KMP 算法。
設 \(X\) 是一個字符串,則以下表述中,\(X_u\) 代表 \(X\) 的第 \(u\) 個字符,\(X_{u \sim v}\) 代表 \(X\) 的從 \(u\) 起到 \(v\) 結束的字串。
首先定義一個字符串的公共前後綴爲這個字符串的一個 \(border\),最長公共前後綴稱爲最長 \(border\)。特別的,不認爲字符串本身是自身的 \(border\)。
性質:字符串 \(S\) 的 \(border\) 的 \(border\) 一定是 \(S\) 的 \(border\),正確性顯然。因此不斷地跳最長 \(border\) 可以遍歷字符串的所有 \(border\)
例如,對於字符串 \(abaab\) 來說,其唯一的 \(border\) 是 \(ab\)。
暴力匹配兩個字符串,時間複雜度爲 \(O(|S||T|)\),考慮優化這個算法。
假設當前匹配時 \(S\) 掃描到了第 \(i\) 位, \(T\) 掃描到了第 \(j\) 位,且 \(S\) 從 \(i\) 向前 \(j\) 位組成的字符串與 \(T\) 的前 \(j\) 位相同,而 \(S_{i + 1} \neq T_{j+1}\),我們稱爲發生了失配。
考慮失配時,指針 \(i\) 不變,只有將指針 \(j\) 前移,纔可能令下一位成功匹配。由於 \(i\) 不變,所以下一個可能發生匹配的字符串一定是 \(T_{1 \sim j}\) 的某個前綴 \(T_{1 \sim k}\) 滿足
\[T_{1 \sim k} = S_{i - k + 1 \sim i}\]
其中由於 \(T_{1 \sim k}\) 是 \(T_{1 \sim j}\) 的字串,一定有 \(k < j\)。由於 \(S_{1 \sim i}\) 的後 \(j\) 位與 \(T\) 的前 \(j\) 位匹配,又有 \(k < j\),因此 \(T_{1 \sim j}\) 的後 \(k\) 位一定與 \(S_{1 \sim i}\) 的後 \(k\) 位即 \(S_{i - k + 1 \sim i}\) 匹配。得出
\[T_{j - k + 1 \sim j} = S_{i - k + 1 \sim i}\]
上面兩個式子等量代換得到
\[T_{1 \sim k} = T_{j - k + 1 \sim j}\]
由 \(border\) 的定義,我們發現 \(T_{1 \sim k}\) 一定是 \(T_{1 \sim j}\) 的 \(border\)。根據 \(border\) 的性質,我們只需要不斷的跳 \(T_{1 \sim j}\) 的最長 \(border\) 即可找到一個最長的可以與 \(S_{1 \sim i}\) 的後幾位匹配的字串。因此問題轉化爲了如何求一個字符串 \(T\) 的所有前綴的最長 \(border\)。
顯然 \(border_1 = 0\)。從第 \(2\) 位開始,我們發現問題等價於用 \(T\)(模式串) 的一個前綴去匹配 \(T_{1 \sim i}\) (文本串)的一個後綴,求這個後綴最長是多少,而這個問題的解決方法與上面那個問題的方法 完 全 一 致,都是不斷跳 \(border\) 即可。在 \(i\) 與 \(j\) 成功匹配時,記錄 \(border_i = j\)。而在這個問題中,由於 \(j\) 恆小於 \(i\),正向掃描 \(i\) 時,所用到的 \(border\) 值都已經被計算出,因此可以得出正確的結果。
考慮時間複雜度:一個顯然的事實是每次跳 \(border\) 模式串指針 \(j\) 都會至少減少 \(1\),而當且僅當第 \(S_{i+1}\) 與第 \(T_{j+1}\) 匹配時,\(j\) 纔會自增,因此 \(j\) 僅增加了 \(O(|S|)\),因此 \(j\) 只可能減少 \(O(|S|)\) 次,所以跳 \(border\) 的總次數不超過 \(O(|S|)\),而掃描整個文本串需要 \(O(|S|)\) 的時間,因此總時間複雜度 \(O(|S|)\)。
Example
P3375 【模板】KMP字符串匹配
Description
給定一個文本串 \(S\) 和一個模式串 \(T\),求 \(T\) 在 \(S\) 中出現的所有位置,同時要求輸出 \(T\) 的每個前綴的 \(border\) 長度。
Limitations
字符串長度不超過 \(10^6\)
Solution
闆闆題
Code
#include <cstdio>
#include <cstring>
const int maxn = 1000006;
char S[maxn], T[maxn];
int nxt[maxn];
void KMP(char *A, char *B, int x, int y, const bool pt);
int main() {
freopen("1.in", "r", stdin);
scanf("%s\n%s", S + 1, T + 1);
int x = strlen(S + 1), y = strlen(T + 1);
KMP(T, T, y, y, false); KMP(S, T, x, y, true);
for (int i = 1; i <= y; ++i) {
qw(nxt[i], i == y ? '\n' : ' ', true);
}
return 0;
}
void KMP(char *A, char *B, int x, int y, const bool pt) {
for (int j = 0, i = pt ? 1 : 2; i <= x; ++i) {
while (j && (B[j+1] != A[i])) j = nxt[j];
if (B[j+1] == A[i]) ++j;
if (!pt) nxt[i] = j;
if (j == y) {
qw(i - y + 1, '\n', true);
}
}
}