kmp算法用于解决寻找母字符串S的子串P
以下为原题,也是kmp能解决的经典问题
代码如下:(无注释)
#include<iostream>
using namespace std;
int const N = 1e5+10,M = 1e6+10;
int n,m;
char s[M],p[N];
int ne[N];
int main()
{
cin>>n>>p+1>>m>>s+1;
//计算next数组
for (int i = 2,j = 0;i<=n;i++)
{
while(j && p[i] != p[j+1]) j = ne[j];
if (p[i] == p[j+1]) j++;
ne[i] = j;
}
//匹配kmp
for(int i = 1,j = 0;i<=m;i++)
{
while(j && p[j+1] != s[i]) j = ne[j];
if (p[j+1] == s[i]) j++;
//if (j == 0),continue;这时候直接让i进一位,所以不写
if (j == n)
{
printf("%d ",i-n+1-1);
j = ne[j];
}
}
}
有注释:
#include<iostream>
using namespace std;
int const N = 1e5+10,M = 1e6+10;
int n,m;
char s[M],p[N];
int ne[N];
int main()
{
cin>>n>>p+1>>m>>s+1; //这里把两个字符串全部输入进去,下标从1开始
//求next过程
for(int i = 2,j = 0;i <= n;i++) //这里自动有ne[1]=0,ne[2]=0
{
while(j && p[i] != p[j+1]) j = ne[j];
//循环退出有两种情况
if (p[i] == p[j+1]) j++;
//if (j == 0),直接i++,所以不需要写任何东西
ne[i] = j;
}
//kmp匹配过程
for(int i = 1,j = 0;i <= m;i++)
{
//只要前方一个数不相同,就一直退
while(j && p[j+1] != s[i]) j = ne[j];
//循环退出有两种可能,一是j退无可退(j = 0),二是不再满足p[j+1] != s[i]
//其中j = 0 的情况不需要写任何代码:if (j == 0),直接i++,不需要写任何东西
if (p[j+1] == s[i]) j++;
if (j == n) //匹配成功
{
printf("%d ",i-n+1-1);//本题要求输出下标从0开始,故-1
//匹配成功后,j若再+1就一定不匹配了,所以直接令j = ne[j]寻找下一个可能匹配的字符串
j = ne[j];
}
}
return 0;
}
暴搜做法
#include<iostream>
using namespace std;
int main()
{
for(int i = 1;i<=m;i++)
{
bool flag = true;
for(int j = 1;j <= n;j++)
{
if(p[j] != s[i+j-1])
{
bool flag = false;
break;
}
if(flag) printf("%d",i);
}
}
}
可以看到,代码在不断地循环计算,复杂度为O(m*n),不可接受
优化做法
先看第一幅图,当前母串s的蓝色部分可以和模版串p的一些部分匹配,但是从某一位开始匹配不上
如果我们不想使用暴力搜索,那就要思考一个问题:绿色模版串至少向前前进几位才有可能再次匹配上?
我们如果让绿色模版串前进p位后,有三种可能
1.前进后一位都匹配不上,直接pass
2.前进后能匹配上一些位数,如第二幅图,但是这样是无效的,尽管有一些段是匹配的,但只要有一位不匹配,那整个串就不可能匹配成功
3.第三幅图的情况,绿色模版串向前移动一定位数后, 除了上次匹配失败的那一位,其他所有位数都可以匹配成功,我们要找的就是这种情况
然而这时候会出现一个问题:
Q1:绿色模版串向前移动p位,可能有多个p可以使绿色串移动后是图3的情况,那么我们需要哪一种呢?
答案是我们要找到最小的p,因为题目的输出要求是:每当模版串成功匹配时,输出母串中成功匹配部分的第一个数的下标。只有我们使用最小的p才保证不会漏解。
至于这个最小的p如何寻找,就是kmp的核心,next数组
kmp的核心:next数组
我们先定义一下前缀和后缀,假设现在有一个字符串“abababababa”(编号从1开始)
长度为3的前缀就是aba,长度为4的前缀就是abab
也就是:从第一个字母开始长度为某一个数的字符串
再来看后缀,这个字符串的第4为是b,那第四位b的一个长度为2的后缀就是ab,同理,第5位的长度为3的后缀就是aba
第i位的长度为j的后缀就是:从i-j+1到i的字符串
我们接下来再来定义next数组:对于一个字符串的第i位数,next[i]就是最大的使得第i位的前缀后缀相等的数(且长度不能大于或等于i,这个先记下来,不要细究原因),比如上文提到的abababababa字符串,i = 5时,next[5] = 3,因为最大可以取3,使得第五位的前后缀相同
next和p的关系
假设我们现在已经知道了这条绿色模版串的next数组(即对每一位i,都知道next[i]是多少),那每当我们要移动绿色串的时候,要移动的最小位数p应该是多少呢?
答案是p = i - next[i],移动i - next[i]位
举个例子:(注意这里上方的是母串,下方位模版串,这里的i和上文的i不同)
不要管为什么是i-1和j对应,i和j+1对应,这个不重要,只是一种习惯
现在我们正在尝试匹配模板串的第j+1位和母串的第i位,c和b匹配失败,这里j = 3,j+1 = 4
下一次可能让模版串匹配成功的情况,需要将模版串向前移动2位,也就是j - next[j](j是3,next[j]是1)
以上是为了理解模版串的移动,实际写代码中我们可以直接让j = next[j],达到同样的效果
正向看一遍
现在母串的蓝色部分和模版串的前一半绿色部分是成功匹配的,但是母串的红圈位置与模版串的绿圈位置的字母不匹配,我们这时候应该怎么做?
答案是向前移动j - ne[j]位
ps:这里ne就是next数组,取名为ne是为了防止一些头文件冲突
什么是移动?
我们接下来要把图上字符串的移动转变为代码
现在在母串s上有一个指针i,i会遍历整个母串,对于每个i,我们要将s[i]与p[j+1]比较,绿色模版串的“移动”实际上就是改变“比较的是第几位”,因此直接令 j = ne[j]就可以达到移动j - ne[j]的效果
这里举一个匹配的例子,以帮助大家更好地理解kmp的匹配过程
只看蓝色的字,不要管上面绿色的线段
//kmp匹配过程
for(int i = 1,j = 0;i <= m;i++)
{
//只要前方一个数不相同,就一直退
while(j && p[j+1] != s[i]) j = ne[j];
//循环退出有两种可能,一是j退无可退(j = 0),二是不再满足p[j+1] != s[i]
//其中j = 0 的情况不需要写任何代码:if (j == 0),直接i++,不需要写任何东西
if (p[j+1] == s[i]) j++;
if (j == n) //匹配成功
{
printf("%d ",i-n+1-1);//本题要求输出下标从0开始,故-1
//匹配成功后,j若再+1就一定不匹配了,所以直接令j = ne[j]寻找下一个可能匹配的字符串
j = ne[j];
}
}
结合代码理解:
第一步,可以看到s[i]与p[j+1]匹配成功,那么i与j都向前移动一位
第二步,s[i]与p[j+1]匹配失败,则令j = ne[j],ne[j] = 4,移动6-4=2位,也就是j = ne[j]
如果一直j = ne[j]且始终匹配不成功,则j最终会等于0,同样跳出while循环,此时只需要让i前进1位即可
如果j = n了,说明p的每一位全部匹配成功,输出匹配成功的字符串的首位数下标,也就是i-n+1,但是本题要求下标从0开始,那就再-1即可,无伤大雅
这里注意一旦j匹配成功,那么j后面就已经没有数字了,为了寻找下一个可能匹配的数,应该令j = ne[j]
next数组的计算
next数组的计算不需要手动遍历,可以用递推取得,方式与kmp匹配的过程非常类似,就是用模版串p自己匹配自己
这里与母串s完全无关,模版串p每一位i的ne[i]仅与p自己有关,因为这里在找p中每一位的最大公共前后缀
先明确目标和手段,我们要求第i位的最大公共前后缀,我们采用递推的方法,所以已知1~i-1位的ne[]数组(类似第二类数学归纳法)
如果蓝色部分已经匹配成功,我们想知道第i位的最大公共前后缀ne[i],是否需要重复求解呢?
答案是不需要,我们只需要比较p[i]与p[j+1],如果相同,则i与j都向后移动一位,记录ne[i] = j即可
相应的,如果p[i]!= p[j+1],只需令j = ne[j]比较下一个可能匹配成功的数即可
结合代码理解:
//求next过程
for(int i = 2,j = 0;i <= n;i++) //这里自动有ne[1]=0,ne[2]=0
{
while(j && p[i] != p[j+1]) j = ne[j];
//循环退出有两种情况
if (p[i] == p[j+1]) j++;
//if (j == 0),直接i++,所以不需要写任何东西
ne[i] = j;
}
如果j不是0,就比较p[i]与p[j+1],成功匹配则退出循环,j++,记录ne[i] = j
匹配失败则令j = ne[j],再次尝试匹配,不断循环
如果循环一直失败,直到j = 0,j会最后匹配一次,如果成功,j++,ne[i] = 1,如果又失败,那说明这一位的最大公共前后缀是0,直接i++即可
(因为我们的ne[]数组是设置在main函数外,所以不操作默认是0,直接i++跳过即可)
以下再举个例子帮助理解
以此这样遍历一遍就可以得到p数组中每一位的ne值
复杂度分析
可以看到,指针i只进不退,j指针虽然指的是模版串p,但是图中可以看到,j指针指向母串s的位置同样也是只进不退,因此复杂度O(m+n),远远优于暴力搜索的O(m*n)