学习记录:【洛谷 P3375 【模板】KMP字符串匹配】-------KMP算法

本文详细介绍了KMP算法的原理和应用,通过实例解释了如何利用KMP算法提高子串在主串中匹配的效率,避免了朴素算法中的重复比较。KMP算法的关键在于构造next数组,用于存储子串中前i个字符的最大公共前后缀长度。通过next数组,当匹配失败时,可以快速找到新的起始位置,从而减少时间复杂度。
摘要由CSDN通过智能技术生成

题外话:一年多没碰程序,最近试着捡起来,发现老师当年摁着我写的总结有奇效,所以…

回到正题,先上代码

#include<bits/stdc++.h>
using namespace std;
char s1[1000010];
char s2[1000010];
int next[1000010];
int f[1000010];
int n1,n2;
int ls;
int ans;
int main()
{
	cin>>s1>>s2;
	n1=strlen(s1);
	n2=strlen(s2);
	for(int i=1;i<n2;i++)
	{
		ls=next[i-1];
		while(ls!=0)
		{
			if(s2[0+ls]==s2[i])
			{
				next[i]=ls+1;
				break;
			}
			else
			{
				ls=next[ls-1];
			}
		}
		if(ls==0&&next[i]==0)
		{
			if(s2[0]==s2[i])
			{
				next[i]++;
			}
		}
	}
	if(s1[0]==s2[0])
	{
		f[0]=1;
	}
	ls=f[0];
	for(int i=1;i<n1;i++)
	{
		while(ls!=0)
		{
			if(ls==n2||s1[i]!=s2[ls])
			{
				ls=next[ls-1];
			}
			else
			{
				f[i]=ls+1;
				break;
			}
		}
		if(ls==0)
		{
			if(s2[0]==s1[i])
			{
				f[i]=1;
			}
		}
		ls=f[i];
	}
	for(int i=0;i<n1;i++)
	{
		if(f[i]==n2)
		{
			cout<<(i+2)-n2<<endl;
		}
	}
	for(int i=0;i<n2;i++)
	{
		cout<<next[i]<<" ";
	}
	return 0;
}

代码写的不是很好看,但勉强还是能算得上一辆小破车,毕竟他能跑不是?

接下来说算法:KMP算法不难,但是能讲清楚却好像是一件很难的事情。简单来说,KMP主要运用于求解子串在主串中出现的位置以及在主串中出现过几次。

我们很容易想到一个朴素算法,将子串逐个和主串的字符比较,如果有错误,就从子串的开头重新比较,这样的复杂度是n*m。

那么有没有更优的解法呢?

废话,当然有,不然我发这博客干啥

不难发现,在朴素算法中,一旦出现错误,便要从头再次开始比较,就好像那种要求一命通关的游戏,一旦失误…

胜败乃兵家常事,大侠请重新来过。

这实在是太糟糕了,如果要是能设置存档点,那该多好啊!

别说,这个还真可以有。

像下面这个例子:

EBDCEBDCEBDAEEAACEBDCEBDAE
EBDCEBDAE

可以发现,在我们第一次辛辛苦苦匹配到主串第八个字符的时候,由于第八个字符子串为‘A’,主串为’E’,匹配宣告失败。按照我们朴素的“一命通关”的想法,则是又从主串的‘B’开始匹配,麻烦。

而我们仔细观察,可以发现子串的前六个字符的前半部分和后半部分是一样的,都是"EBD”,也就是说,如下图:

EBDCEBDC
EBDCEBDAE

如果我们匹配能匹配到第七个字符而在第八个字符出错的时候,这个时候,我们就不需要重新开始了,我们能够使用这个片段的一些已匹配字符,如下图:

EBDCEBDC
XXXXEBDCEBDAE

总结来说就是:若存在 子串中共有Z个字符,其中前X个字符与后X个字符完全相同(Z==2X+1Y),若在主串中也出现这样的片段,那么当我们将子串向后拖拽X+Y个字符,就会使得子串前X个字符与主串该段中后X个字符相同。

我再举个例子:假设有一个这样的游戏,游戏需要你输入一串相对应的方向键,一旦出现一个对不上就必须要重头再来。

游戏要求输入:↑ ← ↑ → ↑ ← ↑
而你输入成了:↑ ← ↑ ← ↑ ← ↑

那么实际上,此时你的输入栏应该是这样的:

↑ ← ↑

你只需要再输入这些就可以了。

→ ↑ ← ↑

现在我们明白了为什么要求出子串每一个前Z(Z为1~子串长度)个字符中的前X个字符和后X个字符完全相同的最大X值(X<Z),那么我们现在就要讲讲怎么将其求出来。

for(int i=1;i<n2;i++)
	{
		ls=next[i-1];
		while(ls!=0)
		{
			if(s2[0+ls]==s2[i])
			{
				next[i]=ls+1;
				break;
			}
			else
			{
				ls=next[ls-1];
			}
		}
		if(ls==0&&next[i]==0)
		{
			if(s2[0]==s2[i])
			{
				next[i]++;
			}
		}
	}

我运用了一个next数组,用来存储子串前i个值中前X个字符与后X个字符完全相同的最大X值(X<i)。

由于使用的读入是cin,所以导致了字符数组下标是从0开始的。

采用了一个临(lin)时(shi)变量ls。

事实上,我们判断字符串前X个字符与后X个字符是否完全相同也不需要重新判断,能在原有值上继续求解。

i值增加1时,判断新加入的字符与第X+1个字符是否,相同有两种情况:

1.相同,则:next[i]==next[i-1]+1

2.不同,则:我们应该尝试将X变更为next[X]的值,继续判断,直到X==0,此时我们就只需要判断新加入的字符是否和第一个字符相同就行了。

至于情况2中为什么将X的值变更为next[X]的值而不是在 next[X]~X间的其他值,这里就不给出证明了,有兴趣的可以试试用反证法证明。

我在这可以给出结论,若有可行解,next[X]一定为仅次于X的最大值。

所以KMP的步骤总共分为两步:

1.将子串中的next值全部求出。
2.将子串和主串进行比较,如果不匹配,则通过next值进行部分回退。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值