KMP算法笔记

蒟蒻学习了(KMP)算法之后的一点总结


目录:

  • 初识(KMP)算法

  • (KMP)算法的思想

  • $KMP $算法的程序实现

  • (KMP)算法的板子题

  • 总结


(1.) 初识(KMP)算法

​ 首先,什么是(KMP)算法?

(KMP)算法是用来处理字符串匹配问题的。也就是给你两个字符串,你需要回答:(B)串是否为(A)串的子串?(也就是(A)串是否包含(B)串)

​ 举个例子:

字符串(A=GCAKIOIIMOICOIBO),字符串(B=GCAKIOI),字符串(C=GCtqlorz)

我们说(B)(A)的子串,而不能说(C)(A)的子串

​ 一般来说,解决这类问题用的方法是暴力枚举

枚举从(A)串的什么位置起开始于(B)串匹配,然后验证是否完全匹配,假设(A)串长度为(n)(B)串长度为(m),那么这个算法的复杂度较为可观,是(O(nm))

​ 但是······

也有例外,比如,当(A=aaaaaaaaaaaaaaaaab)(B=aaaaaaab)时,枚举方法的复杂度就不能能够满足程序执行的需求。

​ 所以有什么解决方法吗?

当然有,(KMP)算法就是用来解决这个问题的,这个算法可以保证在最坏情况下,时间复杂度仍能为(O(n))。当然,在这里(mleq n)


(2.) (KMP)算法的思想

​ 叨叨了这么多,到底(KMP)算法是啥样的呢?

我们假设(A=gcgcgcggcgcak)(B=gcgcgak),接下来我们根据算法流程手推一下(KMP)算法

​ 算法思路是这样的:

定义两个变量(i)(j),表示(A[i-j+1···i])(B[1···j])完全相等。也就是说,(i)是不断增加的,随着(i)的变化,(j)也不断地变化。且满足(A[i])结尾的长度为(i)的字符串正好匹配(B)串的前(j)个字符

现在,我们需要检验(A[i+1])(B[j+1])的关系。很显然,如果(A[i+1]=B[j+1])(i)(j)各加一,表示继续向前检验如果(A[i+1] eq B[j+1])(KMP)算法的思路是调整(j)的位置,也就是减小(j)的值,使得(A[i-j+1···i])(B[1···j])保持匹配并且继续程序(j=m)时,(B)就是(A)的子串啦

手推第一步:

根据上面的流程,我们直接跳到(i=j=5)的情况上

(i) 1 2 3 4 5 6 7 8 9 ···
(A) (g) (c) (g) (c) (g) (c) (g) (g) (c) ···
(B) (g) (c) (g) (c) (g) (a) (k) ··· ··· ···
(j) 1 2 3 4 5 6 7

此时,(A[6] eq B[6]),也就是说,此时(j)不能等于5了,我们要把它改成一个更小的值(j'),于是我们想到将(j)改为3,也就是能使两个字符串继续匹配的最大(j)。那么我们就需要预处理出一个数组(p[i])表示处理到(j)(j+1)不能匹配时要转移到的最大(j)

手推第二步:

继续程序,我们来到了(i=7,j=5)的情况上

(i) 1 2 3 4 5 6 7 8 9 ···
(A) (g) (c) (g) (c) (g) (c) (g) (g) (c) ···
(B) (g) (c) (g) (c) (g) (a) (k) ···
(j) 1 2 3 4 5 6 7

此时,(A[8] eq B[6]),我们要继续程序,(p[5]=3),所以新的(j)值为3;

手推第三步:

继续程序,我们来到了(i=7,j=3)的情况上

(i) 1 2 3 4 5 6 7 8 9 ···
(A) (g) (c) (g) (c) (g) (c) (g) (g) (c) ···
(B) (g) (c) (g) (c) (g)
(j) 1 2 3 4 5

此时,(A[8] eq B[4]),我们要继续程序,(p[3]=1),所以新的(j)值为1;

手推第四步:

​ 继续程序,我们来到了(i=7,j=1)的情况上···

​ 限于篇幅,下面的部分大家自己手推一下,我就不往上写了


(3.) (KMP)算法的程序实现

​ 解决了程序的流程问题,接下来我们看一下程序的核心部分,其实程序的实现并不是很难,只要理解了程序的流 程,其他都好说(其实也好背)

先是(KMP)算法的核心部分,即处理字符串匹配的部分

	j=0;//j的值最初为0,即从0开始匹配A串
	for(int i=0;i<n;++i)//将A串从头扫到尾
	{
		while(j>0&&B[j+1]!=A[i+1])//如果已经进行匹配并且B[j+1]不能与A[i+1]进行匹配
            j=p[j];//令当前的j值为p[j],p[j]的定义前面有讲到
		if(B[j+1]==A[i+1])//如果能继续匹配下去	
            ++j;//继续下一个匹配
		if(j==m)//如果整个B串完全匹配
		{
			printf("%d
",i+1-m+1);//输出B串所在的位置(不要问我为什么是i+1-m+1)
			j=p[j];//继续进行匹配,因为可能还有子串被包含
		}
	}

接下来是(KMP)算法的另一个重要环节——处理(p)数组

我们可以发现,处理(p)数组的代码与处理字符串匹配的代码很相像(甚至只背一个部分就可以啦)。其实,(p)数组的处理就是一个(B)串自我匹配的过程,所以与处理两个字符串匹配的代码蜜汁相像

	p[1]=0;//如果B串从第一个字符开始就不能匹配,那么就将B串向右移一位
	for(int i=1;i<m;++i)//将B串从头扫到尾(注意这里的从头扫到尾并不是从0开始,而是从1开始,因为p数组的处理是B串的自我匹配。如果从1开始,那么每一个字符都会匹配。这样一来就无法求出p数组,从而影响整个程序)
	{
		while(j>0&&B[j+1]!=B[i+1])	//如果已经进行匹配并且B[j+1]不能与B[i+1]进行匹配
            j=p[j];//令当前的j值为p[j]
		if(B[j+1]==B[i+1])	//如果能继续匹配
            ++j;//将进行匹配的B串向右移一位
		p[i+1]=j;//这里是最重要的,就是说当前的第i个字符要转移到上一个能够继续匹配的地方,也是上一个能自我匹配的地方
	}

(4.) (KMP)算法的板子题

题目描述:

给出两个字符串(s_1)(s_2),其中(s_2)(s_1)的子串,求出(s_2)(s_1)中所有出现的位置。

为了减少骗分的情况,接下来还要输出子串的前缀数组(next)

(如果你不知道这是什么意思也不要问,去百度搜([KMP])学习一下就知道了。)

题目分析:

就是(KMP)的板子题,这里面的(next)数组就是我们费尽心机求的(p)数组,那么在程序结束时直接输出(p)数组就好了(qwq)

#include<cstdio>
#include<iostream>
#include<cstring>

using namespace std;

char A[1000010],B[1000010];
int p[1000010];//p数组要开的和B数组一样大
int m,n,j;

int main()
{
    scanf("%s",A+1);//A+1表示从1开始读入A串,下同
    scanf("%s",B+1);
    n=strlen(A+1);
    m=strlen(B+1);
    p[1]=0;
    for(int i=1;i<m;++i)//处理p数组
    {
        while(j>0&&B[j+1]!=B[i+1])	j=p[j];
        if(B[j+1]==B[i+1])	++j;
        p[i+1]=j;
    }
    j=0;//一定别忘记将j值清零
    for(int i=0;i<n;++i)//字符串匹配
    {
        while(j>0&&B[j+1]!=A[i+1])	j=p[j];
        if(B[j+1]==A[i+1])	++j;
        if(j==m)
        {
            printf("%d
",i+1-m+1);
            j=p[j];
        }
    }
    for(int i=1;i<=m;++i)	printf("%d ",p[i]);//输出p数组
    return 0;//恭喜你通过了这道题!!!
}

(5.) 总结

其实(KMP)算法并不是很难,但是程序中的小细节特别多。总之就是一句话,还得练。

原文地址:https://www.cnblogs.com/juruohqk/p/11047170.html