KMP自動機
分類:字符串
內容:詳細版
前置知識
不會的可以點擊鏈接(如果有)或者前往 OI-Wiki 學習
- KMP
一些約定
- 字符集大小默認為
m
- 模板字符串默認為
s
- 文本字符串默認為
t
|s|
指字符串s
的長度- 字符串下標默認從
1
開始
簡介
KMP自動機主要用於字符串的匹配問題,預處理復雜度為O(|s|*m)
,可以以嚴格O(|t|)
的復雜度進行字符串匹配(KMP
為均攤O(|t|)
),並且可以處理可持久化字符串匹配問題。
同時KMP
自動機也是AC
自動機(可以處理多個模板串的匹配)的基礎。
構造KMP
自動機
KMP
自動機與KMP
的區別在於KMP
自動機額外求出了 \(trans_{i,j}\) 表示在第i
位置上往后匹配一個j
字符會轉移到什么狀態(狀態在這里指已經成功匹配了多少個字符)。
在下文中,將用fail
來代替KMP
的next
,nxt
代替上面的trans。
普通地實現KMP
自動機
假設我們處理到了第i
個狀態並且前i-1
個狀態已經完全處理好了,當前的fail
也指向了正確的位置。
考慮每個nxt
指向的狀態:
nxt[s[i + 1]]
顯然指向i+1
。
其余的nxt
應當指向一直跳fail
后第一個下一個字符能匹配的位置,即:
nxt[i][j] = i;
while(nxt[i][j] && s[nxt[i][j] + 1] != j) nxt[i][j] = fail[nxt[i][j]];
if(s[nxt[i][j] + 1] == j) nxt[i][j] = nxt[i][j] + 1;
但是這樣我們沒有用到之前求出來的nxt
並且復雜度很高,所以我們需要找到一種能用到之前求好了的nxt
來快速計算當前nxt
的方法。
考慮nxt[fail[i]][j]
,這個表示的是fail[i]
這個狀態匹配一個j
字符會轉移到什么狀態,即我們想在第i
個狀態后接一個j
字符,但是s[i + 1]
不是這個字符,我們就在fail[i]
這個狀態后面接着找並且找到了一個狀態可以轉移。
仔細分析一下這個東西就是我們要求的nxt[i][j]
。
證明一下:由於nxt[fail[i]][j]
要么是從nxt[fail[fail[i]]][j]
轉移過來的,已經考慮過考慮跳多次fail
,要么是直接在fail[i]
后面接一個j
字符,不需要跳多次fail
,所以不用管跳多次fail
,那么nxt[fail[i]][j]
就是我們要求的nxt[i][j]
了。
// 假設字符串長度為 n,字符集大小為 m
// nxt 一開始都是 0
fail[1] = 0;
nxt[0][s[1]] = 1;
for(int i = 1; i < n; i ++) {
for(int j = 0; j <= m; j ++) {
if(s[i + 1] == j) nxt[i][j] = i + 1;
else nxt[i][j] = nxt[fail[i]][j];
}
}
for(int i = 0; i <= m; i ++)
nxt[n][i] = nxt[fail[n]][i];
這樣寫出來又長細節又多,一下沒搞好就錯了,最重要的是不好背,我們想辦法縮成單獨一個for
循環。
好寫又好背的板子
首先我們處理第i
個狀態時,我們可以先讓所有的nxt[i]
都是nxt[fail[i]]
,然后在第i+1
個狀態再把nxt[i][s[i+1]]
設為i+1
,這樣我們就可以不用把第n
個狀態單獨拿出來
然后我們可以發現在求nxt[i]
的時候不需要用到之前的fail
所以我們可以不記錄所有的fail
值
for(int i = 1, fail = 0; i <= n; i ++) {
fail = nxt[fail][s[i]]; // 注意這一行不能和下一行互換
nxt[i - 1][s[i]] = i;
for(int j = 0; j < m; j ++)
nxt[i][j] = nxt[fail][j];
}
字符串匹配
構造完以后匹配就很簡單了,直接一直沿着nxt
走就行了