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

浙公网安备 33010602011771号