KMP 是一种用来处理字符串匹配问题的算法。
很明显,可以这样暴力解决字符串匹配问题:( 为 的长度, 为 的长度)
for(int i=1;i<=n-m+1;i++)
{
bool f=true;
for(int j=1;j<=m;j++)
{
if(S[i+j-1]!=T[j])
{
f=false;
break;
}
}
if(f)
{
printf("%d\n",i);
}
}
但是这样的算法时间复杂度是 的,肯定会 TLE。
考虑改进这个算法,很容易发现,下面这两个字符串匹配时:
S:ACACBEAC
T:ACACE
模式串匹配到 E
时我们发现失配了,但是这时我们可以不仅仅把 移动一位,而是可以直接移动两位:
S:ACACBEEAC
T:^^ACACE
因为 的前四位已经匹配了,而在这前四位中,前两位构成的前缀和后两位构成的后缀相同,所以我们可以移动两位。
设 表示 的前 个字符构成的字符串,定义一个串 的 border 为满足 且既是 的前缀也是 的后缀的串。那么当 的前 位已匹配,第 位失配时,就可以将 设置成 的最长的 border 的长度。
那么我们可以用 表示 的最长 border 的长度,那么匹配的过程就可以变成这样:
int j=0;
for(int i=1;i<=n;i++)
{
while(j!=0&&T[j+1]!=S[i]) // 如果失配,那么不停移动模式串
{
j=kmp[j];
}
if(T[j+1]==S[i]) // 能匹配就匹配
{
j++;
}
if(j==m) // 匹配成功!
{
printf("%d\n",i-m+1);
}
}
而 数组的求解就可以看作是 和自己匹配:
kmp[1]=0; // kmp[1] 肯定为 0
int j=0;
for(int i=2;i<=m;i++) // T 和自己匹配
{
while(j!=0&&T[j+1]!=T[i]) // 失配则不断移动模式串
{
j=kmp[j];
}
if(T[j+1]==T[i]) // 匹配
{
j++;
}
kmp[i]=j; // 当前匹配的长度就是 kmp[i]
}
完整代码:
// P3375
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n,m;
char S[1000005],T[1000005];
int kmp[1000005];
int main()
{
scanf("%s%s",S+1,T+1);
n=strlen(S+1);
m=strlen(T+1);
kmp[1]=0; // kmp[1] 肯定为 0
int j=0;
for(int i=2;i<=m;i++) // T 和自己匹配
{
while(j!=0&&T[j+1]!=T[i]) // 失配则不断移动模式串
{
j=kmp[j];
}
if(T[j+1]==T[i]) // 匹配
{
j++;
}
kmp[i]=j; // 当前匹配的长度就是 kmp[i]
}
j=0;
for(int i=1;i<=n;i++)
{
while(j!=0&&T[j+1]!=S[i]) // 如果失配,那么不停移动模式串
{
j=kmp[j];
}
if(T[j+1]==S[i]) // 能匹配就匹配
{
j++;
}
if(j==m) // 匹配成功!
{
printf("%d\n",i-m+1);
}
}
for(int i=1;i<=m;i++)
{
printf("%d ",kmp[i]);
}
printf("\n");
return 0;
}
这样做的时间复杂度是 的,是不是比暴力优很多。
时间复杂度证明:
首先预处理 数组是 的,因为 总共只会移动 次,然后每次跳 至少会让 减少 ,所以最多回跳 次。
匹配的时间复杂度是 的,具体原理同上。
一些扩展
-
的 border 一定是 ,所以可以建一棵根节点为 的树,其中 的父亲是 , 的所有祖先 的 都是 的 border。
-
想要求 长度小于等于 或者 的最长的 border 也可以用类似求 数组的方法来求:
num[1]=0; for(int i=2,j=0;i<=n;i++) { while(j!=0&&a[j+1]!=a[i]) j=kmp[j]; if(a[j+1]==a[i]) j++; while(j*2>i) j=kmp[j]; num[i]=j; }
这段代码就是求 表示 长度小于等于 的最长的 border 的长度。
这样做的时间复杂度是 的,证明类似求 数组的时间复杂度的证明。
-
字符串 的最小重复子串(最短的字符串 满足 )也能用 数组求,若 则最小重复子串的长度即为 ,否则 的最小重复子串就是它本身。
证明很简单。