概述
给定一个文本 $A$ 和一个字符串 $B$ ,我们可以利用 KMP 算法尝试找到并展示 $B$ 在 $A$ 中的所有出现(occurrence)。
模板
预处理前缀函数
预处理出字符串 $B$ 的前缀函数 $next$ 数组,其中 $next[i]$ 为既是子串 $B[1…i]$ 的前缀同时也是该子串的后缀的最长真前缀长度。一个字符串的真前缀是其前缀但不等于该字符串本身。根据定义, $next[1]=0$ 。
算法流程
- 在循环中以 $i=2$ 到 $i=n$的顺序计算前缀函数 $next[i]$ 的值。($next[1]$ 被赋值为0)
- 为了计算当前的前缀函数值 $next[i]$,我们令变量 $j$ 表示右端点位于 $i-1$ 的最长匹配前后缀的长度。初始时 $j=next[i-1]$ 。
- 通过比较 $B[j+1]$ 和 $B[i]$ 来检查长度为 $j+1$ 的后缀是否同时也是一个前缀。如果二者相等,那么置 $next[i]=j+1$,否则减少 $j$ 至 $next[j]$ 并重复该过程。
- 如果 $j=0$ 并且仍没有任何一次匹配,则置 $next[i]=0$ 并移至下一个下标 $i+1$ 。
F.A.Q
算法流程中的步骤三,为什么要将 $j$ 减少至 $next[j]$ ?
考虑我们正在计算的 $next[i]$ 。我们要使 $k$ 最大化,并且保证 $B[1…k]$ 与 $B[i-k+1…i]$ 相等。将其拆成两部分看,我们需要在 $B[1…k-1]$ 与 $B[i-k+1…i-1]$ 相等的同时,保证 $B[k]=B[i]$ ,且 $k$ 取到最大值。
不难发现,如果只需要最大化 $k-1$ 使得 $B[1…k-1]$ 与 $B[i-k+1…i-1]$ 相等,我们可以很快得出答案。回顾一下前缀函数的定义即可发现,记 $j$ 为 $next[i-1]$,则此时的 $j$ 即为我们需要最大化的 $k-1$ 的值。如果这时又恰好满足 $B[j+1]=B[i]$ ,则我们需要最大化的 $k$ 即为 $j+1$ 。
然而此时如果不能满足 $B[j+1]=B[i]$ ,我们就只能考虑减小 $j$ 的值。在减小 $j$ 值的同时,我们要始终保证减小后的 $j’$ 满足 $B[1…j’]=B[j-j’+1…j]$ 。
结合上图不难看出,要使得 $B[1…j’]=B[i-j’…i-1]$,即保证 $B[1…j’]=B[j-j’+1…j]$ 。而 $j’$ 的确定也十分简单,再次结合前缀函数的定义可得, $j’$ 的取值应为 $next[j]$ 。
代码实现
1 | next[1] = 0; |
在目标串中查找子串
计算出 $f$ 数组,其中 $f[i]$ 为既是子串 $B[1…i]$ 的前缀同时也是子串 $A[1…i]$ 的后缀的最长前缀长度。(注意这里不一定是真前缀)在预处理前缀函数的过程中,相当于 $B$ 串与自己本身做了一次模式匹配,因此此处的算法流程与上一个操作十分类似。
算法流程
- 在循环中以 $i=1$ 到 $i=n$的顺序计算 $f[i]$ 的值。
- 为了计算当前的 $f[i]$ ,我们令变量 $j$ 表示右端点位于 $i-1$ 的最长匹配前后缀的长度。初始时 $j=f[i-1]$ 。
- 通过比较 $B[j+1]$ 和 $A[i]$ 来检查 $B$ 串中长度为 $j+1$ 的前缀是否也是 $A$ 串中长度为 $j+1$ 的后缀。如果二者相等,那么置 $f[i]=j+1$,否则减少 $j$ 至 $next[j-1]$ 并重复该过程。
- 如果 $j=0$ 并且仍没有任何一次匹配,则置 $f[i]=0$ 并移至下一个下标 $i+1$ 。
- 如果 $j=M$,即找到 $B$ 在 $A$ 中的一次出现。
代码实现
1 | for (int i = 1, j = 0; i <= N; ++i) { |