记录自 10.15 讲课。我们不生产讲义,我们只是讲义的搬运工。

文本串 SS,找模式串 TT(串的模式匹配问题)

S=S= ababab,T=T= abab → 匹配位置:11,33

定义 n=∣S∣n=|S|,m=∣T∣m=|T|

  • Brute Force(朴素算法)

Si…i+m−1=TS_{i\dots i+m-1}=T → ii 位置为一个答案

i,j ← 0
ans ← emptyset
s[n+1]←'#' t[n+1]←'*'
while i<n
. if s[i+1]=t[j+1]
. . i←i+1, j←j+1
. . if j=m
  . . . ans ← ans U {i-m+1}
. else
. . i←i-j+1, j←0

最好:O(m)O(m) 平均:O(n−m)O(n-m) 最坏:O((n−m)m)O((n-m)m)

→ 要求降低最坏情况复杂度

KMP 算法设计思想

假设朴素算法进行过程中,遇到 Si+1≠Tj+1S_{i+1}\neq T_{j+1}(j>0j>0)

观察,若 T1…jT_{1\dots j} 长为 L=j−1L=j-1 的前、后缀不等,则 Si−l+1…i−l+m≠TS_{i-l+1\dots i-l+m}\neq T,故此时可跳过 i←i−li\larr i-l,直接令 i←i−l+1i\larr i-l+1(原本朴素算法这样做)

更一般地:若 T1…jT_{1\dots j} 的长 l(0<l<j)l(0<l<j) 的前、后缀不相等,则 Si−l+1…i−l+m≠TS_{i-l+1\dots i-l+m}\neq T。

找到 [0,j)[0,j) 中最大整数 kk 使得 T1…jT_{1\dots j} 的长为 kk 的前、后缀相等。

对于所有 k+1≤l≤j−1k+1\leq l \leq j-1,Si−l+1…i−l+m≠TS_{i-l+1\dots i-l+m}\neq T。

接下来应该去做的是:对 x=i−kx=i-k,检查 Sx+1…x+m=TS_{x+1\dots x+m}=T?

法一:令 i←i−k,j←0i\larr i-k,j\larr 0,然后逐位比较。

法二(更聪明的做法):令 ii 不变,j←kj\larr k。kk 见上述定义。

π\pi (最长 border 长度)的定义及 KMP 主过程描述

注意:π\pi 的定义与 nxt 数组稍微有差异。(这是老师说的)

对 0<j≤m0<j\leq m,令 πj\pi_j 表示 [0,j)[0,j) 中最大整数 使得 T1…jT_{1\dots j} 的长为 πj\pi_j 的前、后缀相等。

假设 π\pi 已算好,匹配可改为以下算法:

i,j ← 0
ans ← emptyset
while i<n
. if s[i+1]=t[j+1]
. . i←i+1 j←j+1
. . if j=m
. . . ans ← ans U {i-m+1}
. . . j ← pi[j]
. else if j=0
. . i ← i+1
. else
. . j ← pi[j]
  • 复杂度分析
  1. ii 不会回溯,i←i+1i\larr i+1、j←j+1j\larr j+1 执行 ≤n\leq n 次。
  2. i←i+1i\larr i+1 执行 ≤n\leq n 次意味 j←πjj\larr\pi_j 最多执行 nn 次。

→ 因此复杂度 O(n)O(n) (m≤nm\leq n 时)【难点:jj 不是单增的】

因 ii 不回溯,可以“流水作业”式从输入一边读入一边查找。

  • 小结

[0,j)[0,j) 中最大整数 πj\pi_j 使得 T1…jT_{1\dots j} 的长为 πj\pi_j 的前、后缀相等。

π1…m\pi_{1\dots m} 叫做 failure function(失配转移函数),partial match table(部分匹配表)

π\pi 只和 TT 有关。

→ 如何计算 π\pi?

π\pi 的计算

πi+1=j+1←→\pi_{i+1}=j+1 \larr \rarr

jj 是满足如下性质的最大整数:T1…j+1T_{1\dots j+1} 是 T1…i+1T_{1\dots i+1} 的真后缀(不与原串相等的后缀)。(注意 jj 可能为 −1-1,即 jj 不存在)

πi+1\pi_{i+1} 的计算方法:

找到满足如下性质的最大整数 jj:

(1) T1…jT_{1\dots j} 是 T1…iT_{1\dots i} 的真后缀

(2) Tj+1=Ti+1T_{j+1}=T_{i+1}

当存在 j≥0j\geq 0,πi+1=j+1\pi_{i+1}=j+1

若 jj 不存在则 πi+1=0\pi_{i+1}=0

→ 问题转化为,给定 i>1i>1,找出最大 jj 使得

(1) T1…jT_{1\dots j} 是 T1…iT_{1\dots i} 的真后缀

(2) Tj+1=Ti+1T_{j+1}=T_{i+1}

思想:从小到大依次找出满足(1)的各个 jj,直到 jj 满足(2)就确定答案并推出求 πi+1\pi_{i+1} 的子过程。

j ← pi[i]
while true
. if t[j+1]=t[i+1]
. . pi[i+1] ← j+1
. . break
. else if j=0
. . pi[j+1] ← 0
. . break
. else
. . 找到比j更小的最大的j'使得T[1,j']是T[1,i]真后缀。
. . j ← j'

定理:{j∣T1…j是T1…i的真后缀}=\{j|T_{1\dots j}是T_{1\dots i}的真后缀\}=

{πi,ππi…0}\{\pi_i,\pi_{\pi_i}\dots 0\} 对所有 i>0i>0 成立。

证明:πi=0\pi_i=0 显然成立。

假设 πi>0\pi_i>0

注意:{j<i′∣T1…j是T1…i的真后缀}\{j<i'|T_{1\dots j}是T_{1\dots \color{#e00}i}的真后缀\}

$=\{j<i'|T_{1\dots j}是T_{1\dots \color{#e00}i'}的真后缀\}$

={πi′,ππi′…0}=\{\pi_{i'},\pi_{\pi_{i'}}\dots 0\}(根据归纳法)

因此 {j∣T1…j是T1…i的真后缀}=\{j|T_{1\dots j}是T_{1\dots i}的真后缀\}=

$=\{\pi_{i'},\pi_{\pi_{i'}}\dots 0\}∪\{i'\}=\{\pi_{i},\pi_{\pi_i}\dots 0\}$

于是将 j′=πjj'=\pi_j 填入倒数第二行,求 πi+1\pi_{i+1} 的过程转化为

j ← pi[i]
while true
. if t[j+1]=t[i+1]
. . pi[i+1] ← j+1
. . break
. else if j=0
. . pi[j+1] ← 0
. . break
. else
. . j ← pi[j]

求所有 π\pi 的过程即为:

i←1 j←0
pi[1] ← 0
while i<m
. if t[i+1]=t[j+1]
. . i←i+1 j←j+1
. . pi[i] ← j
. else if j=0
. . i ← i+1
. . pi[i] ← j
. else
. . j ← pi[j]
  • 小结

先利用模式串 TT 自己与自己比较得到 π\pi,再利用 π\pi 高效匹配 SS。最坏复杂度 O(n+m)O(n+m)

启示:Reflect yourself before judging others.

了解 KMP 主过程的重要性质

主过程与预处理 π\pi 的过程非常相似,why?

因为某种原因,该段已被删减。

KMP 的扩展应用

π\pi 树 / 失配树

所有 πi\pi_i 向 ii 连边形成的以 00 为根的树。

SS 的两个前缀的最长公共 border 可转化为对应 π\pi 树上两点的 LCA。

Z 算法(扩展 KMP)

【注意,这一段中下标从 00 开始,有些奇怪。】

i>0i>0 时,Z 函数 ziz_i 的定义为:最大整数 ziz_i 使得 T0…n−1T_{0\dots n-1}、Ti…n−1T_{i\dots n-1} 的长为 ziz_i 的二前缀相等。(也就是 T0…n−1T_{0\dots n-1} 和 Ti…n−1T_{i\dots n-1} 的最长公共前缀长度)

在该算法中,从 11 到 n−1n-1 顺次计算 ziz_i。计算 ziz_i 的过程中,利用已算好的 z0…i−1z_{0\dots i-1} 加速。(当然实际上应该z0=nz_0=n,但因为计算中 S0…n−1=S0…n−1S_{0\dots n-1}=S_{0\dots n-1} 的条件啥用没有,所以不妨让 z0=0z_0=0)。

对于 ii,称区间 [i,i+zi−1][i,i+z_i-1] 是 ii 的匹配段(Z-Box)。

计算 ziz_i 时,维护右端点最靠右的已知匹配段(记作 [l,r][l,r],已知即需满足 l≤il \leq i)。根据定义,Sl…rS_{l\dots r} 是 SS 的前缀。初始时设 l=r=0l=r=0(一切还在未知中)。在计算 ziz_i 过程中:

  • 若 i≤ri\leq r,则根据 [l,r][l,r] 定义有 Si…r=Si−l…r−lS_{i\dots r}=S_{i-l\dots r-l},因此 zi≥min⁡(zi−l,r−i+1)z_i\geq\min(z_{i-l},r-i+1) (和 r−i+1r-i+1 取 min 是因为 rr 右边的部分还不知道是什么,只能靠暴力来探索了。)即:
    • 若 zi−l<r−i+1z_{i-l}<r-i+1 则 zi=zi−lz_i=z_{i-l}。
    • 若 zi−l≥r−i+1z_{i-l}\geq r-i+1 则让 zi≤zi−lz_i\leq z_{i-l} 然后暴力尝试扩展直到不能再扩展为止。
  • 若 i>ri>r,那么 ii 的位置是没被探索过的,直接暴力尝试扩展。

每次求出 ziz_i 后,若 i+zi−1>ri+z_i-1>r(ii 开头匹配段的右段到达了前人未曾涉足的地方),则让 l←i,r←i+zi−1l\larr i,r\larr i+z_i-1。

时间复杂度:O(n)O(n)

例题

CF126B Password(KMP π\pi 数组或 Z 函数)

P5829 【模板】失配树 这题已经 SPFA 了,大致题意如下:给定一长为 n≤106n\leq 10^6 的字符串 SS,m≤5×105m\leq 5\times 10^5 次询问,每次询问它的两个前缀的最长公共 border。

P5910

HDU2087

POJ1406