【原创】kmp算法学习笔记

KMP算法

KMP算法的作用

KMP算法是由三个首字母分别为K,M,P的计算机学家发明的。
主要是拿来解决以下问题的:

有一个长度为M的字符串A和一个长度为N的字符串B。问B这个字符串完整地出现在了A字符串的哪个位置。

当然,一般来说,M远大于N,不过要是不远大于也未尝不行。

引入几个专业的名称,称A串为文本串,B串为模式串,也有称为母串子串、称A为主串,称B为关键字的。
那么KMP的过程就是我们常说的字符串匹配,一种关键字查询,的过程。

举例:
文本串:

那么KMP的过程就是我们常说的字符串匹配,一种关键字查询,的过程。

模式串:

过程

结果:

“过程”一词在文本串中 [ 6 , 7 ] [6,7] [6,7] [ 30 , 31 ] [30,31] [30,31]出现。

大概就这样。

这个过程叫字符串匹配,我越说越糊涂了。

字符串匹配的朴素算法

很容易的想到 Θ ( M N ) \Theta(MN) Θ(MN)的算法:

for(int i=0;i<M;i++{
	bool flg=0;
	for(int j=0;j<N && !flg;j++)
		if(A[i+j]!=B[j]) flg=1;
	if(flg) {/*记录答案*/}
}

可以理解为枚举模式串的起点,然后一位位的枚举看接下来的N个字符是不是一样的。
大概是这样的:

朴素字符串匹配

也可以理解为将模式串摆在文本串的下面,开始比较,如果匹配失败,则将模式串后移一位,再次比较。

这样理解也可以写出如下代码:

for(int i=0,j=0;i<n;i++)
{
	if(j==m) {/*记录答案并回溯*/}
	if(A[i]!=B[j]) /*回溯*/  i=i-j+1,j=0;
	else i++,j++;
}

大概是这样吧。

试图优化朴素算法

我说,如果失配(匹配失败),非要“将模式串后移一位”吗?
比方说:
试图优化第一步

为什么呢?
不妨这么想:

假设你就是模式串,你只看得到你所经过的文本串。就像这样:
试图优化第二步——JOJO的奇妙比喻
你发现,至少自己和文本串的前4个字符是不匹配的,剩下的,不知道,接下来要往前走几步呢?
\endline
答案很显然,当然是选择方案二——因为刚刚写了better!
唔姆(微笑掩饰着尴尬的表情)……
答案是——
试图优化第三步
如果采用方案二,你知道,至少你的第一位是匹配的,这里才有可能成功匹配,如果往后走一步,那么肯定是不匹配的,浪费时间。

那么,总之,kmp算法就是,在朴素字符串匹配的基础上,通过某种方案,来让每次失配后模式串最有效率地移动

也可以说是对于之前给出的代码2,回溯时,i不变,j减小到某个合适的值

KMP算法

字符串最长公共前后缀

字符串的前后缀

这个……
随便举个例子就过了吧:

字符串:%%%sdltqlwsl%%% (圣诞了,天气冷,我睡了)

长度前缀后缀
1%%
2%%%%
3%%%%%%
4%%%sl%%%
5%%%sdsl%%%
6%%%sdlwsl%%%
7%%%sdltlwsl%%%
8%%%sdltqqlwsl%%%
9%%%sdltqltqlwsl%%%
10%%%sdltqlwltqlwsl%%%
11%%%sdltqlwsdltqlwsl%%%
12%%%sdltqlwslsdltqlwsl%%%
13%%%sdltqlwsl%%sdltqlwsl%%%
14%%%sdltqlwsl%%%%sdltqlwsl%%%
15%%%sdltqlwsl%%%%%%sdltqlwsl%%%
最长公共前后缀

就是指最长的那个既是前缀又是后缀的字符串,当然不得是其本身。

比方说:
% % % s d l t q l w s l % % %   → % % % a a a a a a   →   a a a a a a b c d a b c   →   a b c \%\%\%sdltqlwsl\%\%\% ~ \to \%\%\% \\ aaaaaa~\to~aaaaa \\ abcdabc~\to~abc %%%sdltqlwsl%%% %%%aaaaaa  aaaaaabcdabc  abc
之类的。

KMP算法的流程

KMP
某年(1977年), K K K先生、 M M M先生、 P P P先生联合发表了一篇文章,他们认为,前文所说:“每次失配后最有效率的移动”,“ j j j降到某个合适的值”,移动的距离就是——假设在模式串上j号位上失配,则令 j = n x t [ j ] j=nxt[j] j=nxt[j](尽管我个人喜欢用 f a i l [ j ] fail[j] fail[j] f a i l [ j ] fail[j] fail[j]代表模式串前 j − 1 j-1 j1个字符的最长公共前后缀。)并给出证明以及计算 f a i l fail fail数组的方法。

(小声BB: KaTeX \KaTeX KATEX加粗不了汉字……)

所以kmp算法大概就是这么个东西:

inline int kmp()
{
	for(int i=0,j=0;;i++,j++)
	{
		if(j==m) return i-j+1;
		if(i==n) return -1;
		while(j!=-1 && a[i]!=b[j]) j=fail[j];
	}
}

其中 r e t u r n   − 1 ; return ~ -1; return 1;代表匹配失败, r e t u r n   i − j + 1 ; return ~ i-j+1; return ij+1;代表找到了第一次成功匹配的位置。

fail数组/nxt数组

f a i l [ j ] fail[j] fail[j]代表:
①第 j j j位失配以后, j j j应该去往的地方。(看上面代码3)
模式串 i i i个字符即 b [ 0 , 1 , ⋯   , j − 1 ] b[0,1,\cdots,\bold{j-1}] b[0,1,,j1]的最长公共前后缀长度。

其中 f a i l [ 0 ] = − 1 \bold{fail[0]=-1} fail[0]=1

正确性

为什么说移动到最长公共前后缀是有效率的?
。。。

懒得说了,比较显然吧。

如果不显然的话就是我的问题。

可计算性

假设我们已经求出了前 j − 1 j-1 j1 f a i l fail fail,现在要求 f a i l [ j ] fail[j] fail[j]
对了别忘了, f a i l [ j ] fail[j] fail[j]代表模式串 i i i个字符即 b [ 0 , 1 , ⋯   , j − 1 ] b[0,1,\cdots,\bold{j-1}] b[0,1,,j1]的最长公共前后缀长度,是 b [ 0 , 1 , ⋯   , j − 1 ] \bold{b[0,1,\cdots,j-1]} b[0,1,,j1]的最长公共前后缀长度,是 b [ 0 , 1 , ⋯   , j − 1 ] \bold{b[0,1,\cdots,j-1]} b[0,1,,j1]的最长公共前后缀长度。
重要的事情说三遍。——尼采《善恶的彼岸》

那么:
计算fail

为什么呢?

举例举例,这里有贪心的一点点想法:

在这里插入图片描述

我们想要求得 0 → j − 1 0\to j-1 0j1最长公共前后缀,我们希望借助前面的 f a i l fail fail,尽快求出尽量大的公共前后缀。

我们自然会想:
如果,上一个求出来的最长公共前后缀,如果这俩的后一个字符相同的话,那岂不是很开心,现在要求的最长公共前后缀就是上一个求出来的最长公共前后缀+1。
如果相同就很开心

如果不相等?
就是说,这个最长公共前后缀的长度它小于 f a i l [ j − 1 ] fail[j-1] fail[j1],我们要要重新找后缀了,怎么做呢?把后缀的长度--,然后看这个长度的前后缀是不是相等的?这显然不行。那怎么办?
我们找最长公共前后缀一定是在某个之前的最长公共前后缀的长度+1得来的(-1+1=0),所以我们可以只看已经求出的 f a i l fail fail。只有长度 < f a i l [ j − 1 ] \lt fail[j-1] <fail[j1] f a i l fail fail,就有可能成为新的最长公共前后缀。于是我们就走 f a i l fail fail链,这样就可以避免一些无用的查询了。

大家可以很明显地看出哪些是我之前写的哪些是我之后写的,认真和水一目了然。

KMP的代码

#include<cstdio>
#define maxn 1000005
#define maxm 10005
int T,n,m,a[maxn],b[maxm],fail[maxm];
inline void getfail()
{
	fail[0]=-1;
	for(int i=0,j=-1;i<m;fail[++i]=++j)
		while(j!=-1&&b[i]!=b[j]) j=fail[j];
}
inline int kmp()
{
	for(int i=0,j=0;;i++,j++)
	{
		if(j==m) return i-j+1;
		if(i==n) return -1;
		while(j!=-1&&a[i]!=b[j]) j=fail[j];
	}
}
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&n,&m);
		for(int i=0;i<n;i++) scanf("%d",&a[i]);
		for(int i=0;i<m;i++) scanf("%d",&b[i]);
		getfail();
		printf("%d\n",kmp());
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值