KMP这道题,有一个非常重要且烧脑的数组:next[ ]数组,一旦将这个数组的含义理解透彻了,那么KMP类似的题也就能迎刃而解了。
先将介绍一下待会会用到的几个定义。字符串前缀与后缀。
举个例子:以字符串abcaba来解释
从第一个字母向后枚举时,此过程的前缀与后缀分别为:
"a": 前缀:空集,后缀:空集 "ab": 前缀:a,后缀:b
"abc":前缀:a,ab;后缀:c,bc "abca":前缀:a,ab,abc 后缀:a,ca,bca
"abcab":前缀:a,ab,abc,abca;后缀:b,ab,cab,bcab
"abcaba":前缀:a,ab,abc,abca,abcab;后缀:a,ba,aba,caba,bcaba
那么我们可以发现对于字符串abcaba,前缀与后缀有相等的请况,我们将其记录下来
"a": 前缀:空集,后缀:空集 无相等 ;ne数组不变 "ab": 前缀:a,后缀:b 无相等 ;ne数组不变 "abc":前缀:a,ab;后缀:c,bc 无相等;ne数组不变 "abca":前缀:a,ab,abc 后缀:a,ca,bca 相等"a",长度为1;ne[4] = 1
"abcab":前缀:a,ab,abc,abca;后缀:b,ab,cab,bcab 相等"ab",长度为2;ne[5] = 2
"abcaba":前缀:a,ab,abc,abca,abcab;后缀:a,ba,aba,caba,bcaba 相等"a",长度为1; ne[6] = 1
在这里引出的前缀与后缀 是next[ ]数组的核心。
接下来这张图较为抽象,可先查看后续的模拟,会更直观。看完模拟再来查看此图会更清晰
假设有一母串(红线),与一子串(紫线)。
当一开始匹配时,母串的j ~ j1与字串的i0 ~ i0+n-1匹配,而继续向下遍历时,最后一个字符i0 + n不与母串的j1 +1匹配,那么造成匹配失败。
字串向后移动,以寻求匹配,当移动到蓝线时,此时匹配成功,我们可以发现:
上述三条线的黑色部分的字符串相等,又因为子串自己本身的①与②的字符串相等,那么②与③的字符串相等。
由此可见,倘若我们能通过某种标记使得在第一次匹配不成功时,能立马让字串的前缀②与母串的③进行匹配上,那么我们只需要继续匹配字串的蓝线部分即可,省去了大量的时间来匹配①的字符串。
注意了! 我们此时仔细观察字串的第一种状态(紫线),②与①相等(自己本身的前缀),而②与紫线的黑色部分(后缀)相等,那么我们就可以通过前后缀相等的方式,在第一次匹配不成功时,让第一种状态(紫线)直接跳转到第二种状态(蓝线),进而省去了匹配黑色部分字符串的时间,只需匹配剩余蓝线的字符串即可。
举个具体的例子:假如子串为上述"abcaba",母串为"abdabcabd",那么我们在进行匹配时:
代码:
/*
kmp
1、先想一下暴力做法,2、如何去优化
1、for(循环母串)
for(循环字串)
*/
#include<bits/stdc++.h>
using namespace std;
int n,m;
//s表示子串,p表示母串
char s[100010],p[1000010];
//ne[]数组是记录的子串的前后缀相等的情况
int ne[100010];
int main()
{
cin >> n >> s+1 >> m >> p+1;
//寻找子串前后缀相等的情况
for(int i = 2, j = 0; i <= n; i++)
{
//当子串的前后缀出现了不相等的情况,那么就查看下一组是否相等如果都不想等,那么j = 0,退出while循环
while(s[i] != s[j + 1] && j) j = ne[j];
//如果相等,那么就向后移动
/*
比如aba,一开始的s[2]!=s[0+1],由于j = 0,所以不会进入while循环,也不会进入if语句
此时记录下ne[2] = 0;
i++;
因为此时s[3] == s[0+1],仍不会进入while循环,但是会进入if循环,使得j++;
此时记录下ne[3] = 1,接着遍历结束,得到ne[3] = 1;
那么如果是abc的话,遍历结束,ne数组整体都不会改变值,仍为0
*/
if(s[i] == s[j + 1]) j++;
//记录下当前的ne[i]值
ne[i] = j;
}
for(int i = 1,j = 0; i <= m; i++)
{
/*
比如子串aba;母串ababa
对于子串:ne[3] = 1;
那么第一次匹配,输出0,第二次直接令j = ne[j] = 1,
那么可以直接判断p[4]是否与s[1+1]相等,如果不相等,那么就继续后退j = ne[j](退而求其次)
直到找到一组相等的p[i] == s[j+1],如果没找到,那么j=0跳出循环
*/
//同样的,当母串与子串出现了不匹配,那么根据前后缀。找到下一组能直接进行匹配的字符串(退而求其次)
while(p[i] != s[j+1] && j) j = ne[j];
//如果相等,则j向后移动,判断下一个子串与母串的字符是否相等
if(p[i] == s[j+1]) j ++;
//如果已经全部匹配完成
if(j == n)
{
//输出当前匹配在母串的起始下标
cout << i - n << " ";
//根据前后缀,找到下一组能直接进行匹配的字符串
j = ne[j];
}
}
}
哈希方式,看可以参考这一篇:哈希表(hash)
代码:
#include<iostream>
using namespace std;
const int N=1e6+10,P=131;
typedef unsigned long long ULL;
ULL hb[N],ha[N],p[N];
int get(ULL h[],int l,int r)
{
//将高位的减去(这里是指进制下的高低位)
//将字符串转为哈希方式下的公式
return h[r]-h[l-1]*p[r-l+1];
}
int main()
{
int n;
scanf("%d",&n);
char a[N];
scanf("%s",a+1);
int m;
scanf("%d",&m);
char b[N];
scanf("%s",b+1);
p[0]=1;
for(int i=1;i<=m;i++)
{
//预处理,将字符串a与字符串b转化成P进制下的数
p[i]=p[i-1]*P;
hb[i]=hb[i-1]*P+b[i];
ha[i]=ha[i-1]*P+a[i];
}
for(int i=n;i<=m;i++)
{
//套用比较公式
if(get(hb,i-n+1,i)==get(ha,1,n))
printf("%d ",i-n);
}
return 0;
}
KMP这一节我是放到最后才去听,现在前四章都已经学完,明天准备开dp,所以开dp之前把先前所遗留下的问题弥补起来。因为是放到最后才去听,所以学起来没那那么难,主要还是得益于最后一节的哈希匹配。能用KMP求解的题,用哈希一般都能做出来,所以当时听完了哈希,KMP这道题用哈希就立马做了出来。当时的难点也许,也许在现在看来就没有那么难了。