KMP自动机
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
走就行了