记号

设模式串 AA 长度为 nn,主串 BB 长度为 mm

S[l,r]S[l, r] 为字符串 SS 从第 ll 位到第 rr 位构成的字串。

满足 nextinext_i 的条件但不一定最大的 jj 被称为 nextinext_i 的候选项。


KMP 算法,又称模式匹配算法,可以在线性时间内判断字符串 AA 是否是字符串 BB 的子串,还可以查找 AABB 中出现的位置。

KMP 算法分为两步:

  1. AA 进行自我匹配,求出数组 nextnext,其中 nextinext_i 是满足 nexti<inext_i < i 且满足 A[1,nexti]=A[inexti+1,i]A[1, next_i] = A[i - next_i + 1, i] 的最大值(特别的,如果不存在 nextinext_i,则 nexti=0next_i = 0)。
  2. AABB 进行匹配,求出数组 ff,其中 fif_i 是满足 fiif_i \le i 且满足 A[1,fi]=B[ifi+1,i]A[1, f_i] = B[i - f_i + 1, i] 的最大值。

显然当 fi=nf_i = nAABB 中的第 in+1i - n + 1 位开始出现了一次。

nextnext 怎么求呢?

首先,有一个引理:若 j0j_0nextinext_i 的一个候选项,那 <j0< j_0 的最大的 nextinext_i 的候选项是 nextj0next_{j_0}

证明

先证 nextj0next_{j_0}nextinext_i 的候选项。

因为 A[1,j0]=A[ij0+1,i]A[1, j_0] = A[i - j_0 + 1, i],同时取后 nextj0next_{j_0} 个字符得 A[j0nextj0+1,j0]=A[inextj0+1,i]A[j_0 - next_{j_0} + 1, j_0] = A[i - next_{j_0} + 1, i]

因为 A[1,nextj0]=A[j0nextj0+1,j0]A[1, next_{j_0}] = A[j_0 - next_{j_0} + 1, j_0],所以根据 A[j0nextj0+1,j0]=A[inextj0+1,i]A[j_0 - next_{j_0} + 1, j_0] = A[i - next_{j_0} + 1, i] 可以得出 A[1,nextj0]=A[inextj0+1,i]A[1, next_{j_0}] = A[i - next_{j_0} + 1, i],所以 nextj0next_{j_0}nextinext_i 的一个候选项。

再证 nextj0next_{j_0} 是最大的。

使用反证法。假设存在一个 j1j_1nextinext_i 的候选项,且 nextj0<j1<nextinext_{j0} < j_1 < next_i

因为 j0j_0nextinext_i 的候选项,所以 A[1,j0]=A[ij0+1,i]A[1, j_0] = A[i - j_0 + 1, i],同时取后 j1j_1 个字符可以得到 A[j0j1+1,j0]=A[ij1+1,i]A[j_0 - j_1 + 1, j_0] = A[i - j_1 + 1, i]

因为 j1j_1nextinext_i 的候选项,所以 A[ij1+1,i]=A[1,j1]A[i - j_1 + 1, i] = A[1, j_1]A[j0j1+1,j0]=A[ij1+1,i]=A[1,j1]A[j_0 - j_1 + 1, j_0] = A[i - j_1 + 1, i] = A[1, j_1],得到 A[j0j1+1,j0]=A[1,j1]A[j_0 - j_1 + 1, j_0] = A[1, j_1]

此时,j1j_1 也是一个 nextj0next_{j_0} 的候选项,且比 nextj0next_{j_0} 更大,与 nextj0next_{j_0} 为最大候选项的定义不符,故假设不成立,证毕。


综上所述,nexti1next_{i - 1} 如果被计算出来,nexti1next_{i - 1} 的候选项分别为 nexti1next_{i - 1}nextnexti1next_{next_{i - 1}}nextnextnexti1next_{next_{next_{i - 1}}} \dots

而可能成为 nextinext_i 的候选项的值分别为 nexti1+1next_{i - 1} + 1nextnexti1+1next_{next_{i - 1}} + 1nextnextnexti1+1next_{next_{next_{i - 1}}} + 1 \dots,因为 A[1,j]=A[ij+1,i]A[1, j] = A[i - j + 1, i] 的前提条件是 A[1,j1]=A[ij+1,i1]A[1, j - 1] = A[i - j + 1, i - 1]

所以,nextnext 可以这样求:

  1. 初始化 next1=0next_1 = 0

  2. 维护一个变量 jj 从能选到最大的 nexti1next_{i - 1} 往回遍历,直到 j+1j + 1 可以作为 nextinext_i 的候选项为止。(即 Aj+1=AiA_{j + 1} = A_i

  3. 如果连 j=0j = 0 都无法匹配,不存在 nextinext_i,那么 nexti=0next_i = 0

由于定义的相似性,ff 可以直接用相同的方法求解。

初始时,jj 的值为 00;每次循环内,jj 的值最多会增加 11jj 移动的总次数最多为 2n+2m2n + 2m,时间复杂度为 O(n+m)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]);
}