题外话:一年多没碰程序,最近试着捡起来,发现老师当年摁着我写的总结有奇效,所以…
回到正题,先上代码
#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值进行部分回退。