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走就行了
