学习笔记——KMP算法

如下均搬运自花姐姐的博客

闲话:\(KMP\)做为一个经典的字符串算法,天然值得\(OIer\)学习,但每每因其实现难、不常考,致使\(OIer\)不想学(话说这不是人之常情吗),今天,让咱们来解决这一毒瘤,改变咱们遇字符串就炸的现象吧!html

1、何谓模式串匹配

模式串匹配,就是给定一个须要处理的文本串(理论上应该很长)和一个须要在文本串中搜索的模式串(理论上长度应该远小于文本串),查询在该文本串中,给出的模式串的出现有无、次数、位置等。ios

这就至关于\(OJ\)上判断程序对错的过程,\(OJ\)经过匹配每个字符来判断输出结果的正误,也算是一种模式串匹配。git

2、KMP算法的核心思想

首先,咱们来了解一下\(KMP\)算法的由来。度娘说算法

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的。
所以人们称它为克努特—莫里斯—普拉特操做(简称KMP算法)。

然而,这些伟人我一个也不认识,但这并不妨碍它成为一个伟大的算法。数组

首先,让咱们来分析一下咱们朴素算法被\(T\)飞的缘由:咱们用低效的枚举匹配文本串,致使在最坏状况下该算法(好像也不能叫算法)退化成O(文本串长度*匹配串长度),其实也比较好卡,如如下数据函数

文本串:aaaaaaaaaaa......aaaaaaaaab (其中有1e6个a)
模式串:aaaaaaaaaaa......aaaaaaaaab (其中有5e5个a)

那么,咱们要执行\(2.5e6\)次计算,会直接致使超时(TLE)post

既然如此,固然轮到咱们KMP算法闪亮登场了!学习

KMP 的精髓在于,对于每次失配以后,我都不会从头从新开始枚举,而是根据我已经得知的数据,从“某个特定的位置”开始匹配;而对于模式串的每一位,都有惟一的“特定变化位置”,这个在失配以后的特定变化位置能够帮助咱们利用已有的数据不用从头匹配,从而节约时间。优化

举个栗子spa

文本串:abaabab
模式串:aba

正常的算法(暴力)会在第三个字符以后从新开始匹配,但让咱们来看一看KMP算法的运行过程

文本串:abaabab
模式串:   	aba

因此,KMP在每次失配以后就能够跳回以前的某一位,在从该位开始新的一轮匹配。

注意:
1.通常使用模式串来匹配失配数组。

2.匹配位置的肯定。

\(str1\) 中,对于每一位 $str1(i) $,它的 \(kmp\) 数组应当是记录一个位置 \(j, j≤i\)而且知足 $str1(i)=str1(j) \(而且在\)j!=1 \(时理应知足\)str1(1)$至 $ str1(j−1) $ 分别与 $str1(i−j+1)~str1(i−1) $ 的每一位相等。

对于2,咱们能够用前缀和后缀的思想来理解

模式串:abcdabc
前缀:a,ab,abc,abcd,abcda,abcdab,abcdabc
后缀:c,bc,abc,dabc,cdabc,bcdabc,abcdabc

\(kmp\)数组记录到它为止的模式串前缀的真前缀和真后缀最大相同的位置(注意,这个地方没有写错,是真的有嵌套qwq)。

如上面例子中,前缀和后缀的第三项相同,因此\(kmp[7]=3\);

3、讲了这么多,直接上代码吧

1.\(string\)类型的\(KMP\)

int nxt[MAXN];
void getnext(string t){
	int j=0,k=-1;
	nxt[0]=-1;
	while(j<t.size()){
		if(k==-1||t[j]==t[k]) nxt[++j]=++k;
        //1.当k=-1时,确定到顶了,不能再回溯,因此直接赋值
        //2.当t[j]==t[k]时,这个下标的nxt值就是上一个下标的值加1
		else k=nxt[k];//若是尚未找到,就返回上一个可回溯的下标再找
	}
}

void KMP(string s,string t){
	int i=0,j=0;
	while(i<s.size()){
		if(j==-1||s[i]==t[j]){++i;++j;}
        //1.当j=-1时,说明到了边界,把文本串的指针加1,再从新开始新一轮匹配
        //2.当s[i]==t[j]时,说明匹配到了,那就指针日后移,看下一位可否匹配
		else j=nxt[j];//若是没有到边界又没有匹配到,就回溯看可否从新匹配
		if(j==t.size()){printf("%lld\n",i-t.size()+1);j=nxt[j];}
        //当j==t.size()时,说明已经彻底匹配了,输出答案,并回溯匹配其余位置上的合法答案
        //对输出结果的解释:i表示到下标为i的位置时两串彻底匹配,减去(t.size()-1)就是减去模式串的长度,结果就是匹配的起始位置
	}	
}

2.\(char\)数组类型的\(KMP\)

#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#define il inline
#define ll long long
#define gc getchar
#define int long long
#define R register
using namespace std;
//---------------------初始函数-------------------------------
il int read(){
	R int x=0;R bool f=0;R char ch=gc();
	while(!isdigit(ch)) {f|=ch=='-';ch=gc();}
	while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=gc();}
	return f?-x:x;
}

il int max(int a,int b) {return a>b?a:b;}

il int min(int a,int b) {return a<b?a:b;}


//---------------------初始函数-------------------------------

const int MAXN=1e6+10;
char s1[MAXN],s2[MAXN];
int kmp[MAXN];

signed main(){
	scanf("%s%s",s1+1,s2+1);
    //细节:s1+1表示从下标为一的位置开始读入,方便以后的操做
	int lens1=strlen(s1+1),lens2=strlen(s2+1);
    //由于char不像string同样有不少自带函数,因此要用<cstring>库中的函数求长度
	kmp[0]=kmp[1]=0;//初始化
	for(R int j=1,k=0;j<lens2;++j){
		while(k&&s2[j+1]!=s2[k+1]) k=kmp[k];
        //当k>0且s2[j+1]!=s2[k+1]时,说明既没有到边界又没有匹配到,就回溯看可否从新匹配
		if(s2[j+1]==s2[k+1]) ++k;
        //由上面的while循环可知,如今的k必定是匹配的,因此咱们只须要判断这一位可否比上一位多匹配一个字符
		kmp[j+1]=k;//赋值这一位最多能匹配的字符
	}
	for(R int j=0,k=0;j<lens1;++j){
		while(k&&s1[j+1]!=s2[k+1]) k=kmp[k];
        //当k>0且s2[j+1]!=s2[k+1]时,说明既没有到边界又没有匹配到,就回溯看可否从新匹配
		if(s1[j+1]==s2[k+1]) ++k;
        //由上面的while循环可知,如今的k必定是匹配的,因此咱们只须要判断这一位可否比上一位多匹配一个字符
		if(k==lens2){printf("%lld\n",j+1-lens2+1);k=kmp[k];}
        //当k==lens2时,说明已经彻底匹配了,输出答案,并回溯匹配其余位置上的合法答案
        //对输出结果的解释:j表示到下标为j+1的位置时两串彻底匹配,减去(lens2-1)就是减去模式串的长度,结果就是匹配的起始位置        
	}
	for(R int i=1;i<=lens2;++i) printf("%lld ",kmp[i]);
	return 0;
}

好了,\(KMP\)的基本内容到此结束,在加一个时间复杂度分析就完美了。如下引用\(rqy\)的话:

每次位置指针\(i++\)时,失配指针\(j\)至多增长一次,因此\(j\)至多增长\(len\)次,从而至多减小\(len\)次,因此就是\(\Theta\)\(len_N\)+\(len_M\))=\(\Theta\)(N+M)。

其实咱们也能够发现,$ KMP $算法之因此快,不只仅因为它的失配处理方案,更重要的是利用前缀后缀的特性,从不会反反复复地找,咱们能够看到代码里对于匹配只有一重循环,也就是说 \(KMP\) 算法具备一种“最优历史处理”的性质,而这种性质也是基于$ KMP $的核心思想的。

另一篇讲的比较好的博客

OI-wiki上别人推荐的博客

完结撒花了

没想到吧,我又回来了!

做为一名合格的\(OIer\),咱们固然要讲练结合,打出一套合击拳,才能更好的巩固咱们对\(KMP\)算法的理解。

1.来一道裸的\(KMP\)的题(题解)

2.这才是\(KMP\)的板子(题解)

3.\(kmp\)数组新定义(当提高思惟的题作)(题解)(个人代码)

差一点自主作出的紫题,仍是要多注意码代码时的细节

4.\(KMP\)+线性\(DP\)(讲的特别详细的题解)(个人代码)

5.终极难题:\(KMP\)+\(DP\)+矩阵乘法

啊,这题是真的写不动,先咕着吧。

原来KMP还能够求循环子串,学到了学到了,再扔几道例题吧!

1.求最短循环子串(学习\(KMP\)求循环子串的不错的博客+题解)(个人代码)

2.求循环子串数量(讲的不错的题解)(个人代码)

3.求最长循环子串长度和(题解)(个人代码)

(终于上了一道要脑子的题,要用相似并查集路径压缩的优化)