记号
设模式串 A 长度为 n,主串 B 长度为 m。
设 S[l,r] 为字符串 S 从第 l 位到第 r 位构成的字串。
满足 nexti 的条件但不一定最大的 j 被称为 nexti 的候选项。
KMP 算法,又称模式匹配算法,可以在线性时间内判断字符串 A 是否是字符串 B 的子串,还可以查找 A 在 B 中出现的位置。
KMP 算法分为两步:
- 对 A 进行自我匹配,求出数组 next,其中 nexti 是满足 nexti<i 且满足 A[1,nexti]=A[i−nexti+1,i] 的最大值(特别的,如果不存在 nexti,则 nexti=0)。
- 将 A 与 B 进行匹配,求出数组 f,其中 fi 是满足 fi≤i 且满足 A[1,fi]=B[i−fi+1,i] 的最大值。
显然当 fi=n 时 A 在 B 中的第 i−n+1 位开始出现了一次。
next 怎么求呢?
首先,有一个引理:若 j0 是 nexti 的一个候选项,那 <j0 的最大的 nexti 的候选项是 nextj0。
证明
先证 nextj0 是 nexti 的候选项。
因为 A[1,j0]=A[i−j0+1,i],同时取后 nextj0 个字符得 A[j0−nextj0+1,j0]=A[i−nextj0+1,i]。
因为 A[1,nextj0]=A[j0−nextj0+1,j0],所以根据 A[j0−nextj0+1,j0]=A[i−nextj0+1,i] 可以得出 A[1,nextj0]=A[i−nextj0+1,i],所以 nextj0 是 nexti 的一个候选项。
再证 nextj0 是最大的。
使用反证法。假设存在一个 j1 为 nexti 的候选项,且 nextj0<j1<nexti。
因为 j0 是 nexti 的候选项,所以 A[1,j0]=A[i−j0+1,i],同时取后 j1 个字符可以得到 A[j0−j1+1,j0]=A[i−j1+1,i]。
因为 j1 为 nexti 的候选项,所以 A[i−j1+1,i]=A[1,j1],A[j0−j1+1,j0]=A[i−j1+1,i]=A[1,j1],得到 A[j0−j1+1,j0]=A[1,j1]。
此时,j1 也是一个 nextj0 的候选项,且比 nextj0 更大,与 nextj0 为最大候选项的定义不符,故假设不成立,证毕。
综上所述,nexti−1 如果被计算出来,nexti−1 的候选项分别为 nexti−1,nextnexti−1,nextnextnexti−1…;
而可能成为 nexti 的候选项的值分别为 nexti−1+1,nextnexti−1+1,nextnextnexti−1+1…,因为 A[1,j]=A[i−j+1,i] 的前提条件是 A[1,j−1]=A[i−j+1,i−1]。
所以,next 可以这样求:
-
初始化 next1=0。
-
维护一个变量 j 从能选到最大的 nexti−1 往回遍历,直到 j+1 可以作为 nexti 的候选项为止。(即 Aj+1=Ai)
-
如果连 j=0 都无法匹配,不存在 nexti,那么 nexti=0。
由于定义的相似性,f 可以直接用相同的方法求解。
初始时,j 的值为 0;每次循环内,j 的值最多会增加 1;j 移动的总次数最多为 2n+2m,时间复杂度为 O(n+m)。
代码
模板题代码:
#include <cstring>
#include <cstdio>
using namespace std;
int a, b, nxt[1000001];
char s1[1000001], s2[1000001];
int main(){
scanf("%s%s", s1 + 1, s2 + 1);
a = strlen(s1 + 1), b = strlen(s2 + 1);
nxt[1] = 0;
for (int i = 2, j = 0;i <= b;i ++){
while (j && s2[j + 1] != s2[i]) j = nxt[j]; // 不断失配
if (s2[j + 1] == s2[i]) j ++; // 匹配成功
nxt[i] = j;
} // 自我匹配
for (int i = 1, j = 0;i <= a;i ++){
while (j && s2[j + 1] != s1[i]) j = nxt[j];
if (s2[j + 1] == s1[i]) j ++;
if (j == b) printf("%d\n", i - b + 1), j = nxt[j]; // A 在 B 中出现
}
for (int i = 1;i <= b;i ++) printf("%d ", nxt[i]);
}