KMP
posted on 2023-07-16 06:28:16 | under 笔记 | source
upd on 2026:远古文章,较为简单且可能有误。
前言
不会画图 QwQ,有些图需要放大,请见谅。
前置知识
border
定义:在字符串 \(S\) 中,若有长度为 \(k\) 的前缀与长度为 \(k\) 的后缀相等,则称该前缀(后缀)为 \(S\) 的 \(1\) 个 \(\operatorname{border}\)。
一个简单性质:字符串 \(S\) 的 \(\operatorname{border}\) 的 \(\operatorname{border}\) 也是 \(S\) 的 \(\operatorname{border}\)。
证明:

KMP 算法
算法介绍
用于快速查找字符串 \(T\) 在 \(S\) 中的匹配位置。
先谈谈无优化的暴力解法,时间复杂度为 \(O(|S|*|T|)\)。
而 \(KMP\) 可视作暴力算法的优化,但其时间复杂度仅有 \(O(|S|+|T|)\)。
它是怎么优化的呢?假设我们要在 \(abcabcabf\) 中匹配 \(abcabf\)。
回想暴力的流程,首先枚举点 \(i\),然后依次判断 \(S_{i+j-1}\) 是否等于 \(T_j\)。实际上,这部分与 \(KMP\) 是大致相同的。
不过,\(KMP\) 枚举 \(i\)、维护 \(j\)。代表 \(T_1\) ~ \(T_j\) 与 \(S_{i-j+1}\) ~ \(S_i\) 一一对应。
若 \(S\) 与 \(T\) 的对应位不同(称为失配),暴力算法则退出循环,将 \(i\) 前进一位,但 \(KMP\) 则考虑运用已知条件,找到最优继续匹配位置。
定义失配数组 \(nxt[j]\) 表示 \(T\) 长度为 \(j\) 的前缀的最长 \(\operatorname{border}\)(的结尾指针)。
发生失配时,可以通过改变 \(j\) 的值使得 \(S_i=T_{j+1}\)。 因为 \(T_1\) ~ \(T_{nxt[j]}\) \(=\) \(T_{j-nxt[j]+1}\) ~ \(T_j\),所以令 \(j=nxt[j]\),这样既保证正确性又达到优化的效果。
\(KMP\) 的核心就是上面那句话,画图可以帮助理解:
\(i=6\)、\(j=5\) 时,发生失配:
(为啥我列表没了诶!)
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|
| a | b | c | a | b | c | a | b | f |
| 1 | 2 | 3 | 4 | 5 | ||||
| a | b | c | a | b |
\(nxt[4]=2\),将 \(j\) 改为 \(2\):
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|
| a | b | c | a | b | c | a | b | f |
| 1 | 2 | |||||||
| a | b |
继续匹配,直到 \(i=9\)、\(j=6\),\(T\) 已被遍历完,于是匹配
位置为 \(4\):
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|
| a | b | c | a | b | c | a | b | f |
| 1 | 2 | 3 | 4 | 5 | 6 | |||
| a | b | c | a | b | f |
算法结束。
失配数组的建立
可粗略理解为 \(T\) 的自匹配,同样记录 \(i\)、\(j\)。
假设我们已经知道 \(nxt_1\) ~ \(nxt_{i-1}\) 的值,而 \(j=nxt_{i-1}\),接下来怎么推出 \(nxt_i\)?
从定义入手,\(nxt_{i-1}\) 告诉我们:\(T_1\) ~ \(T_j\) \(=\) \(T_{(i-1)-j+1}\) ~ \(T_{i-1}\)。
代数角度较难理解,画图就很清晰了:

显然,只要绿色格子相同,即满足 \(T_{j+1} = T_i\)时,\(nxt_i=j+1\)
假如不相同呢?为了方便表达,定义 \(z=nxt_j\)。
由 \(\operatorname{border}\) 的性质可知,\(T_1\) ~ \(T_z\) 也是 \(T_1\) ~ \(T_{i-1}\) 的前缀的 \(\operatorname{border}\),用图来表示就是这样:

显然,满足 \(T_{z+1} = T_i\) 即可。
以此类推,底层原理就是如此。
代码实现时,我们不需要 \(z\) 等变量,不断令 \(j=nxt_j\) 即可。
代码模板
#include<string>
#include<cstring>
#include<cstdio>
#include<algorithm>
#define sc scanf
#define pr printf
using namespace std;
const int N=1e6+5;
int nxt[N],len1,len2;
char st1[N],st2[N];
void border()
{
int j=0;
nxt[1]=0;
for(int i=2; i<=len2;i++) //注意从2开始枚举
{
while(j&&st2[j+1]^st2[i])
{
j=nxt[j];
}
if(st2[j+1]==st2[i])
{
++j;
}
nxt[i]=j;
}
}
void kmp()
{
int j=0;
for(int i=1; i<=len1;i++)
{
while(j&&st1[i]^st2[j+1])
{
j=nxt[j];
}
if(st1[i]==st2[j+1])
{
++j;
}
if(j==len2)
{
pr("%d\n",i-len2+1);
j=nxt[j]; //为了继续匹配
}
}
}
int main()
{
sc("%s%s",st1+1,st2+1);
len1=strlen(st1+1);
len2=strlen(st2+1);
border();
kmp();
for(int i=1; i<=len2;i++)
{
pr("%d ",nxt[i]);
}
return 0;
}
时间复杂度分析
先讨论 \(KMP\) 主函数的最坏时间复杂度。
首先,\(i\) 总共增加 \(|S|\) 次,\(j\) 只会随 \(i\) 增加而增加,所以 \(j\) 至多增加 \(|S|\) 次。
在最坏情况下,如 \(S=\{aaaa...\}\) 时,\(nxt_j=j-1\),同时 \(j≥0\)。结合上述条件,可知 \(j\) 至多减少 \(|S|\) 次。
于是,主函数的时间复杂度是 \(O(|S|)\);同理,预处理部分的时间复杂度是 \(O(|T|)\)。所以总时间复杂度就是 \(O(|S|+|T|)\)。
失配树
如上面这题,当题目是求两前缀的最长公共 \(\operatorname{border}\) 长度时怎么办?直接枚举会超时,这时失配树就出场了。
利用 \(\operatorname{border}\) 的性质,我们发现 \(S\) 的 \(\operatorname{border}\) 集合可表示为 \(S\) 的最大 \(\operatorname{border}\) 与其 \(\operatorname{border}\) 集合。
因此,\(S\) 的 \(\operatorname{border}\) 集合就是 \(nxt_{|S|}\) 、\(nxt_{nxt_{|S|}}\)、\(nxt_{nxt_{nxt_{|S|}}}\)...... 诶,很像是树中父子节点的关系。
于是,对于每个点 \(i\),建立一条由 \(nxt_i\) 指向 \(i\) 的边,所建成的树称作失配树。
失配树中,对于任意点 \(u\),它的所有祖先节点就是它的所有 \(\operatorname{border}\)。
回顾题目,易发现两前缀的最长公共 \(\operatorname{border}\) ,就是失配树上两结点的 \(LCA\),本题解决。

浙公网安备 33010602011771号