KMP算法

模板题:

给出两个字符串A和B,其中A为B的子串,求出A在B中所有出现的次数。

首先,字符串匹配的朴素算法就是枚举B串的每一个元素,然后当 \(B[i]\)\(A[1]\) 相等时,在枚举 \(B[i+1]\)\(A[2]\)...依次类推,直到A和B的子串完全匹配是输出A所在的位置就OK啦

But,它的时间复杂度是 \(O(nm)\)(n,m是字符串A,B的长度)

当n,m超过一定程度是就要T了

所以KMP出现了


KMP算法是一种可以在线性时间内,判断字符串A是否为字符串B的子串,并求出A在B出现的位置的神奇算法(反正很高级就好了)

不过字符串Hash也可以在 \(O(n)\) 的时间内预处理,并且支持 \(O(1)\) 查询,不过好像莫得KMP方便


下面是KMP与暴力的区别

\(B[i]\)\(A[j]\) 匹配失败后,i只向后移动了一个,这就浪费了很多时间,所以KMP出现了

我们可以预处理出一个next数组,记录匹配失败后下一次的匹配的 \(i\)(如何处理等会讲),然后就可以跳过中间的无用的匹配,就可以节约很多时间

这就好比打游戏的时候,你在Boss关输了,本来要重新开始的,但是你在中途存档了,就可以直接跳关,节约时间

(算阶还有一个 \(f[i]\) 表示B中以 \(i\) 结尾的子串与A的前缀能够匹配的最长长度,这个锅最后补(手动尴尬))

下面进入next数组的计算方法


首先 \(next[i]\) 表示A中以 \(i\) 结尾的非前缀子串与A的前缀能匹配的最长长度

很显然对吧,如果你能看懂的话,那真是太好了,因为你可以跳过下面的理解教程

手动理解中

红色的是以i结尾的非前缀子串,绿色的是A的前缀

所以next数组就是红色为绿色的真子集(绿色包含红色,但绿色不等于红色)时,红色的最长长度

(画个图就好理解多了)

=理解完毕=

怎么用代码实现呢。。。

某蒟蒻lcg(举手):枚举 \(j\)\(i\) 的大小,并查找 \(A[1\sim j]\)\(A[i-j+1\sim i]\) 是否相等

很好,某个菜鸡已经讲出了\(O(n^2)\)的预处理,我们得到了一个比暴力还慢的预处理了

如何在更短的时间里求出next数组呢?

我们首先要证明当 \(next[i]\) 可以为 \(j\) 时,小于 \(j\) 能选择的最大值为 \(next[j]\)

证明:易证,证毕

证明:(不会反证法的左转)

假设有 $next[j_0]<j_1<j_0,使 \(next[i]\) 可以选择 \(j_1\)

如图所示:

明显可以看出 \(next[j_0]\) 是包含在 \(next[j_1]\) 里的

又因为next数组表示在当前位置能匹配到的最大值,而 \(j_1\)\(next[j_0]\) 更优,这与next的定义矛盾,所以假设不正确

因此

\(next[i]\)可以为\(j\)时,小于\(j\)能选择的最大值为\(next[j]\)

然后就可以进行预处理,在求\(next[i]\)时,我们只需要在\(next[i-1]+1,next[next[i-1]]+1...\)找符合要求的值就OK了

代码实现:

初始化\(next[1]=0\),不断尝试匹配\(j\),如果失败,使\(j=next[j]\),直到\(j==0\);如果匹配成功则\(j++\)

next[1]=0;
for(int i=2,j=0;i<=n;i++){
	while(j>0&&b[i]!=b[j+1]) j=next[j];
	if(b[i]==b[j+1]) j++;
	next[i]=j;
}

下面是字符串的比较了(顺带说一下f数组)

for(int i=1,j=0;i<=m;i++){
	while(j>0&&(j==n||b[i]!=a[j+1])) j=next[j];
	f[i]=j;//直接赋值即可
	if(j==n) ans++;//匹配成功
}

最后输出ans即可

原文地址:https://www.cnblogs.com/LCGUO/p/12296600.html