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\),本题解决。

posted @ 2026-01-14 17:34  Zwi  阅读(2)  评论(0)    收藏  举报