KMP算法详解


代码块部分格式不兼容,更好的阅读体验

声明:本文的字符串下标均从1开始,对于某个字符串a,a.substr(i,j)表示a从第i位开始,长度为j的字串

模板题

传送门

KMP算法的大致原理

个人认为其他博客已经讲得很好,这里简单讲,把重点放在next数组上

先推几篇博客:

首先,我们把模板题中的s1串称为文本串,重命名为s,s2称为模式串,重命名为t(本文中不区分s与t的大小写)

设n为s的长度,m为t的长度(会在代码片中出现)

看图,在第一轮匹配中,匹配到了一个不相等的位置,如果用暴力,那就是从头再匹配,但是可以看到T串中有一段重复的“ABC”,无需重复匹配,所以第二轮直接跳到如图所示的位置比较两个蓝色的部分

这就是KMP算法的大致思路

next数组

定义

看了KMP的大致原理,相信大家都产生了疑问:我怎么知道要让T串跳到哪个位置呢?这就要用到next数组了,这是KMP的核心,也是难点

先不用管怎么求next数组,看定义:
令 j = n e x t i , 则 有 j < i 且 t . s u b s t r ( 1 , j ) = = t . s u b s t r ( i − j + 1 , i ) , 且 对 于 任 意 k ( k > j ) , t . s u b s t r ( 1 , k ) ≠ t . s u b s t r ( i − k + 1 , k ) 令j=next_i,则有j<i且t.substr(1,j)==t.substr(i-j+1,i),且对于任意k(k>j),t.substr(1,k)≠t.substr(i-k+1,k) j=nexti,j<it.substr(1,j)==t.substr(ij+1,i),kk>j,t.substr(1,k)=t.substr(ik+1,k)
也就是说,next[i]表示“T中以i结尾的非前缀字串”与“T的前缀”能匹配的最长长度,当不存在这样的j时,next[i]=0

举个例子:

若T=“ABCDABCE”,则对应的next={0 0 0 0 1 2 3 0}

应用

根据next数组的定义,next中存储的是长度,但是由于它是T的某个前缀字串的长度,我们也可以将next当做下标使用(一定要弄清楚,不然后面很蒙)

仍然用上面的图片真懒呐

设S的指针为i,T的指针为j,表示当前完成匹配的位置(也就是说S[i]和T[j]是相等的)

第一轮匹配中,当j==7时,我们发现T的下一位和S的下一位不等,但是T的第57位和13位是一样的,即next[7]=3,所以我们需要将T的指针(j)跳到第3位,也就是j=next[j],这里有一些细节不是很好理解,KMP在实现时是很巧妙的,我们放到整段代码理解:

		while(j != 0 && s[i] != t[j+1])
			j = next[j];
		if(s[i] == t[j+1])
			j++;
		if(j == m){//j==m标志着已经全部完成匹配
			printf("%d\n",i - m + 1);
			j = next[j];
		}

求法

这里是整个KMP最难理解的部分,所以放到最后

先贴出代码

	next[1] = 0;//初始化
	for(int i = 2 , j = 0 ; i <= m ; i++){
		while(j != 0 && t[j+1] != t[i])
			j=next[j];//全算法最confusing的语句
		if(t[j+1] == t[i])
			j++;
		next[i] = j;
	}

考虑暴力枚举:最外层循环枚举每一位i,第二层枚举next[i],里层判断第二层枚举的是否合法

显然,时间复杂度是在O(n2)~O(n3),还不如O(n*m)的暴力匹配

优化求法:

先提前声明:求next[i]是要用到next[1~i-1]的,所以我们要从前向后顺序枚举i

定义“候选项”的概念(可能跟《算法竞赛……》的不大一样):如果j满足 t.substr(1,j)==t.substr(i-1-j-1,j)&&j<i-1则j是next[i]的一个候选项

例子:

绿色表示相等的两个字串,则j是next[i]的一个候选项,若标成蓝色的两个字符相等,则候选项j是合法的,next[i]就是所有合法的j中的最大值+1

很显然,对于next[i]而言,next[i-1]是它的候选项,但是,问题是next[next[i-1]],next[next[next[i-1]]],......都是候选项,为什么呢?还是看图:

假设next[13]=5,根据next的定义,标绿色部分是相等的,再细化一下绿色部分中相等的部分:假设next[5]=2,同理,第二行(不计最上面的下标行)的黄色部分相等,又因为绿色部分相等,我们可以得到第三行的黄色部分都是相等的,再简化为第4行,会发现:这不是和第一行一样了吗(只是长度小了)!

以此类推,可以得到next[i-1],next[next[i-1]],next[next[next[i-1]]],......都是候选项,且他们的值是从左向右递减的,因此,按照这个顺序找到第一个合法的候选值之后,我们就可以确定next[i]

重新看一下代码:

	next[1] = 0;
	for(int i = 2 , j = 0 ; i <= m ; i++){
		while(j != 0 && t[j+1] != t[i])//找到第一个合法的候选项
			j=next[j];//缩小长度
		if(t[j+1] == t[i])
			j++;
		next[i] = j;
	}

发现,每一轮循环没有j=next[i-1]的语句。原因很简单:上一轮结束时语句next[i]=j决定了这一轮刚开始就有j==next[i-1],注意这里的前后的i不一样(都不是同一轮循环了)不要学傻了

时间复杂度

上结论:O(n+m)

以next数组的求值为例:

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

最外层显然是O(m)的,问题是里面

在while循环中,j是递减的,但是又不会变成负数,所以整个过程中,j的减小幅度不会超过j增加的幅度,而j每次才增加1,最多增加m次,故j的总变化次数不超过2m,整个时间复杂度近似认为是O(m)

如果还不能理解,就想像一个平面直角坐标系,x轴为i,y轴为j,从原点出发,i每向右一个单位,j最多向上一个单位,j也可以往下掉(while循环),但不能掉到第四象限,j向下掉的高度之和就是while内语句执行的总次数,是绝度不会超过m的

匹配的循环与上述相近,时间为O(n+m),不在赘述

所以,总的时间复杂度为O(n+m)

模板题代码

不要问模板题输出的最后一行是什么意思,我也不知道,反正输出next数组就对了

#include <iostream>
#include <cstdio>
#include <cstring>
#define nn 1000010
using namespace std;
int sread(char s[]) {
	int siz = 1;
	do
		s[siz] = getchar();
	while(s[siz] < 'A' || s[siz] > 'Z');
	while(s[siz] >= 'A' && s[siz] <= 'Z') {
		++siz;
		s[siz] = getchar();
	}
	--siz;
	return siz;
}
char s[nn];
char t[nn];
int next[nn];
int n , m;
int main() {
	n = sread(s);
	m = sread(t);
	next[1] = 0;
	for(int i = 2 , j = 0 ; i <= m ; i++){
		while(j != 0 && t[j+1] != t[i])
			j=next[j];
		if(t[j+1] == t[i])
			j++;
		next[i] = j;
	}
	for(int i = 1 , j = 0 ; i <=n ; i++){
		while(j != 0 && s[i] != t[j+1])
			j = next[j];
		if(s[i] == t[j+1])
			j++;
		if(j == m){
			printf("%d\n",i - m + 1);
			j = next[j];
		}
	}
	for(int i = 1 ; i <= m ; i++)
		printf("%d " , next[i]);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值