知识点
参考资料:
王道考研视频讲解,对KMP介绍的非常好
ACWING 视频讲解,评论的帮助也很大,记得看评论
阮一峰2013年日志/博客上介绍的KMP
KMP算法:快速模式匹配算法,对人检索字符行为的模拟(具体王道视频大约5分钟开始),主要是目标串的指针不发生回溯,只有模式串的指针会回溯【注解:以下介绍中模式串右移和回溯的意思其实相同,移动的位数=已匹配的字符数 - next数组对应值,回溯的话是指针j直接回溯到以next对应数值为下标的位置,但是接下来用来比较的位置是j+1的位置,代码以回溯实现】,大概步骤如下
KMP算法实现重点:确定每个位置的next数组,因为每次整个模式串移动位数 = 已匹配的字符数 - 对应位置的部分匹配值
有点抽象,看上面的图,如第二次匹配时,串的地址都从1开始,对于“abca”的前缀:a、ab、abc;后缀:bca、ca、a, 则对应的next数组值/对应位置(也就是最后一个a)的部分匹配值为1;所以对于模式串的移动位数为4-1=3,所以模式串向后移动3位,若回溯的话,则j为1,则下一次参加比较的位置是j+1的位置,即2的位置,即 ‘c’;【好绕啊啊啊!加油!】
next数组和对应位置部分匹配值意思相同,当j为最后一个不匹配字符的下标时,我们考虑的next数组的值是不匹配字符的前一个字符的next值,即下标为j-1的字母的值,即考虑[1, j-1]的前后缀问题。
next数组详细介绍:
KMP最主要代码
注意事项:ne数组即next数组,字符集合p为模式串,n为其长度,字符集合s为模板串/目标串,m为其长度;两个字符集合都是从1开始。j除了作为模式串指针外,还统计匹配相同的字符串数字。
cin>>n>>p+1>>m>>s+1; //从1开始储存
// 第一个for循环为针对模式串p产生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;
}
//第二个for循环开始匹配
//i指向目标串,j指向模板串
for(int i=1,j=0;i<=m;i++)//所以i从1开始,j从0开始,真正参与与s[i]比较的是p[j+1]
{
while(j&&s[i]!=p[j+1])
{
j=ne[j];//不能匹配上,则j=ne[j];
}
if(s[i]==p[j+1]) j++;//能够匹配上,则向后移动
if(j==n)
{
printf("%d ",i-n);//输出所有出现位置的起始下标,输出的下标从0开始 所以i-n,而不是i-n+1;
j=ne[j];
}
}
//输出 -- 借助输出来理解会更好理解!
/*
5
abcac
10
ababcabcac
1 2
此时已匹配完的串为“ab” 所以j回到0,下一个p[j+1]为p[1],即b
此时已匹配完的串为“abca” 所以j回到1,下一个p[j+1]为p[2],即c
*/
//若在第二次for输出cout<<s[i]<<" "<<p[j+1]<<" "<<ne[j]<<" "<<j<<endl; 的话
//结果:
/*
a a 0 0
b b 0 1
a c 0 2 不匹配,第一次j变化
b b 0 1
c c 0 2
a a 0 3
b c 1 4 不匹配,第二次j变化
c c 0 2
a a 0 3
c c 1 4
*/
好的,知识点算是告一段落了,来看题目!
题目
先上基础题
AcWing 831. KMP字符串
原题链接
题意:求模式串在主串种出现的所有位置的起始下标(从0开始计数)
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m;
char s[N],p[N];
int ne[N];
int main()
{
cin>>n>>p+1>>m>>s+1;
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;
}
for(int i=1,j=0;i<=m;i++)
{
//cout<<s[i]<<" "<<p[j+1]<<" "<<ne[j]<<" "<<j<<endl;
while(j&&s[i]!=p[j+1])
{
//cout<<j<<" "<<p[j+1]<<" ";
j=ne[j];
//cout<<j<<" "<<p[j+1]<<endl;
}
if(s[i]==p[j+1]) j++;
if(j==n)
{
printf("%d ",i-n);
j=ne[j];//输出所有出现位置的起始下标
}
}
//for(int i=2;i<=n;i++) cout<<ne[i]<<" ";
return 0;
}
P3375 【模板】KMP字符串匹配
洛谷原题链接
题意:输入一个主串和一个模式串,按从小到大输出模式串在主串中出现的位置,并赛最后一样输出模式串的模个整数,第i个整数表示模式串中长度为i的前缀的最长border长度。所谓border,就是定义一个字符串 s 的 border 为 s 的一个非 s 本身的子串 t,满足 t 既是 s 的前缀,又是 s 的后缀。
题解:对于第一个问题直接用KMP,第二个问题其实是next数组,回忆一下,next数组的定义:模式串中,各个子串的“前缀”和“后缀”所共有元素中最长元素的长度。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m;
char s[N],p[N];
int ne[N];
int j;
int main()
{
cin>>s+1;
cin>>p+1;
int ls=strlen(s+1),lp=strlen(p+1);
for(int i=2;i<=lp;i++)
{
while(j&&p[i]!=p[j+1])
{
j=ne[j];
}
if(p[i]==p[j+1]) j++;
ne[i]=j;
}
j=0;
for(int i=1;i<=ls;i++)
{
while(j>0&&s[i]!=p[j+1])
{
j=ne[j];
}
if(s[i]==p[j+1]) j++;
if(j==lp)
{
printf("%d\n",i-lp+1);
//输出所有出现位置的起始下标 ,下标从1开始,其实就相当于上一题的i-n+1
j=ne[j];
}
}
for(int i=1;i<=lp;i++)
{
cout<<ne[i]<<" ";
}
return 0;
}
P4391 [BOI2009]Radio Transmission 无线传输
原题链接
题解:一道神奇的题目,参考这儿,神级题解,因为所给的字符串是S1的一个子串S,S1又是由S2不断拼接而成的,那么对于S1的子串S中必定有不断循环的S2构成的,现在要求S2的最短长度,即子串S-最大公共前后缀长度,即n-next[n];
【注释:以下是完整周期的分析,图中画的第一个线段表示前缀,第二个是原字符串,也是“红色段”+后缀,但是红色段由属于前缀,则由红色段=第一段=第二段…=所有段,则红色串就是最小的循环子串】
对于不是完整周期的分析如下:
【注解:对于下面这个例子,最大公共前后缀为“cabca”,从对完整周期红色段的分析转化为对周期内任意一个字母的分析,假设一个完整周期有n位,不完整周期有y=n-i位,其中第0个周期被切掉了i位,第0个周期最后一个字母=第一个周期最后一个字母=第二个周期最后一个字母=所有周期最后一个字母,则最大公共前后缀肯定比完整周期的最大公共前后缀少了i位,则n-next[n]=(n-i)-next[y]=y-next[y]
,此时的next[y]+i=next[n]
】
这道题其实感觉是思维题了,代码简单但是要发现规律可能需要点能力?但是这道题真的让人觉得next数组和KMP真的太太太强大了!!
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
char a[N];
int ne[N];
int n;
int ans;
int j;
int main()
{
cin>>n>>a+1;
for(int i=2;i<=n;i++)
{
while(j&&a[i]!=a[j+1])
{
j=ne[j];
}
if(a[i]==a[j+1]) j++;
ne[i]=j;
}
ans=n-ne[n];
//for(int i=1;i<=n;i++) cout<<ne[i]<<" ";
cout<<ans<<endl;
}
总结
目前做的题目还不是难题,继续加油吧!!