KMP算法

Published on7/29/2025
Last updated on 12/4/2022
Created on 12/17/2021

KMP算法(Knuth–Morris–Pratt algorithm)是个高效的字符串匹配算法。

方法

假设我们要做以下匹配:

文本串A:A1 B2 A3 B4 A5 B6 A7 A8 B9 A10 B11 A12 C13 B14模式串B:A1 B2 A3 B4 A5 C6 B7\begin{aligned} &\text{文本串}A:\quad \mathtt{ \underset{1}{A}\ \, \underset{2}{B}\ \, \underset{3}{A}\ \, \underset{4}{B}\ \, \underset{5}{A}\ \, \underset{6}{B}\ \, \underset{7}{A}\ \, \underset{8}{A}\ \, \underset{9}{B}\ \underset{10}{A}\ \underset{11}{B}\ \underset{12}{A}\ \underset{13}{C}\ \underset{14}{B} }\\ &\text{模式串}B:\quad \mathtt{ \underset{1}{A}\ \, \underset{2}{B}\ \, \underset{3}{A}\ \, \underset{4}{B}\ \, \underset{5}{A}\ \, \underset{6}{C}\ \, \underset{7}{B} } \end{aligned}

很明显,一开始匹配到第 6\mathtt{6} 位时字符就不同了,传统匹配方法会将模式串往前移一位,再重新一位一位匹配,但我们知道,文本串前 5\mathtt{5} 位是和模式串相同的,如果将模式串往前移动一位后能够匹配成功,肯定有个前提是模式串的第 1\mathtt{1} 位和第 2\mathtt{2} 位相同,不然是肯定匹配不成的,这几次检查就白费了。 那我们该移动几位呢? 模式串和文本串的前 5\mathtt{5} 位是相同的,我们把这一部分称为模式串的前缀 PP ,我们往前移动模式串,肯定要移动到一个有可能匹配成功的位置,我们发现,如果相同部分 PP 的前 kk 位和后 kk 位相同,那我们可以移动模式串使得模式串的前 kk 位与文本串相同部分的后 kk 位匹配,比如,在我们例子里的 PP 中: 前 1\mathtt{1} 位和后 1\mathtt{1} 位相同:

A1 B2 A3 B4 A5 C6 B7\mathtt{ \textcolor{Green}{ \underset{1}{A}\ \, } \textcolor{SpringGreen}{ \underset{2}{B}\ \, \underset{3}{A}\ \, \underset{4}{B}\ \, } \textcolor{Green}{ \underset{5}{A}\ \, } \textcolor{Gray}{ \underset{6}{C}\ \, \underset{7}{B} } }

3\mathtt{3} 位和后 3\mathtt{3} 位相同:

A1 B2 A3 B4 A5 C6 B7\mathtt{ \textcolor{Green}{ \underset{1}{A}\ \, \underset{2}{B}\ \, } \textcolor{OliveGreen}{ \underset{3}{A}\ \, } \textcolor{Green}{ \underset{4}{B}\ \, \underset{5}{A}\ \, } \textcolor{Gray}{ \underset{6}{C}\ \, \underset{7}{B} } }

因此我们可以直接把这 3\mathtt{3} 位对齐,然后直接依次往后比较 A[6]A[6]B[4]B[4]A[7]A[7]B[5]B[5]A[8]A[8]B[6]B[6] 等等

文本串A:A1 B2 A3 B4 A5 B6 A7 A8 B9 A10 B11 A12 C13 B14模式串B:A1 B2 A3 B4 A5 C6 B7\begin{aligned} &\text{文本串}A:\quad \mathtt{ \textcolor{SpringGreen}{ \underset{1}{A}\ \, \underset{2}{B}\ \, } \textcolor{Green}{ \underset{3}{A}\ \, \underset{4}{B}\ \, \underset{5}{A}\ \, } \underset{6}{B}\ \, \underset{7}{A}\ \, \underset{8}{A}\ \, \underset{9}{B}\ \underset{10}{A}\ \underset{11}{B}\ \underset{12}{A}\ \underset{13}{C}\ \underset{14}{B} }\\ &\text{模式串}B:\quad \mathtt{ \qquad \textcolor{Green}{ \underset{1}{A}\ \, \underset{2}{B}\ \, \underset{3}{A}\ \, } \textcolor{SpringGreen}{ \underset{4}{B}\ \, \underset{5}{A}\ \, } \underset{6}{C}\ \, \underset{7}{B} } \end{aligned}

我们对齐尽可能多的位数,比如这里对齐 3\mathtt{3} 位,而不是 1\mathtt{1} 位,这是因为要对齐 1\mathtt{1} 位我们需要往前移动模式串 4\mathtt{4} 位,中途跳过了对齐 3\mathtt{3} 位的情况,这会有漏解的隐患。 KMP 算法的核心是匹配失败后,算法会根据已匹配的模式串字符数量( PP 的长度)来向前移动模式串,使其前缀与文本串中已匹配部分的相同后缀重合,再开始新一轮的比较。算法开始时,我们会预处理出一个数组 kmp[]\mathtt{kmp[]} ,长度与模式串相同, kmp[k]\mathtt{kmp[k]} 表示已经匹配成功 k\mathtt{k} 位了,这时匹配失败,我们可以向前移动模式串,使模式串的前 kmp[k]\mathtt{kmp[k]} 位保持与文本串的对应位置匹配(上述其实跟文本串是啥没关系,因为我们知道当前处理的部分是匹配的),再继续匹配。 这个对齐的过程在 KMP 算法中经常出现,模式串的位置会经常改变,所以我们用两个索引来记录:

i: 正在匹配的文本串字符位置j: 模式串中我们已匹配成功的位置\begin{aligned} \mathtt{i}:\ &\text{正在匹配的文本串字符位置}\\ \mathtt{j}:\ &\text{模式串中我们已匹配成功的位置}\\ \end{aligned}

我们可以表示出来:

文本串A:A1 B2 A3 B4 A5 B6i A7 A8 B9 A10 B11 A12 C13 B14模式串B:A1 B2 A3j B4 A5 C6 B7\begin{aligned} \text{文本串}A&:\quad \mathtt{ \textcolor{SpringGreen}{ \underset{1}{A}\ \, \underset{2}{B}\ \, } \textcolor{Green}{ \underset{3}{A}\ \, \underset{4}{B}\ \, \underset{5}{A}\ \, } }\mathtt{ \overset{ \textcolor{Orange}{\displaystyle{i}} }{ \underset{6}{B} }\ \, }\mathtt{ \underset{7}{A}\ \, \underset{8}{A}\ \, \underset{9}{B}\ \underset{10}{A}\ \underset{11}{B}\ \underset{12}{A}\ \underset{13}{C}\ \underset{14}{B} }\\ \text{模式串}B&:\quad \mathtt{ \qquad \textcolor{Green}{ \underset{1}{A}\ \, \underset{2}{B}\ \, } } \mathtt{ \underset{ \textcolor{Orange}{\displaystyle{j}} }{\textcolor{Green}{ \underset{3}{A} } }}\ \, \mathtt{ \textcolor{SpringGreen}{ \underset{4}{B}\ \, \underset{5}{A}\ \, } \underset{6}{C}\ \, \underset{7}{B} } \end{aligned}

每次对齐后,我们会检查 A[i]A[i] 是否与 B[j+1]B[j+1] 相同,是则继续比较,否则就没必要再比下去了,再对齐一次


以下内容来自KMP2.md

Σ\Sigma 表示字符集,Σ\Sigma^* 表示字符集中字符组成的字符串,cc 表示任意字符,SS 表示任意字符串,MM 表示自动机

对于自动机 MMQQ 表示状态集,qq 表示任意状态,q0q_0 表示初始状态,AQA\subseteq Q 表示接受状态集 δ(q,c):Q×ΣQ\delta(q,c):Q\times\Sigma\to Q 表示转移函数 ϕ(S):ΣQ\phi(S):\Sigma^*\to Q 表示终态函数

对于模式串 PPσP(S):ΣN\sigma_P(S):\Sigma^*\to\N 表示后缀函数,表示:PP 从头匹配,SS 从尾匹配,最大的匹配长度。 严谨地说,是与 PP 某一前缀匹配的 SS 最长后缀之长度。 形式化地说:

σP(S):=max{k P[k ⁣:]=S[: ⁣k] }=max{k PkS }\begin{align*} \sigma_P(S):=&\max\{k|\ P[k\!:]=S[:\!k]\ \}\\ =&\max\{k|\ P_k\sqsupset S\ \} \end{align*}

σP(S[k ⁣:])=P\sigma_P(S[k\!:])=|P| 时,我们便能知道 S[kP+1 ⁣: ⁣k]=PS[k-|P|+1\!:\!k]=P,即 kPk-|P| 为一个有效偏移。

我们想要在自动机上转移时,时刻保证:

#ϕ(S[k ⁣:])=σP(S[k ⁣:])(1) ^\#\phi(S[k\!:])=\sigma_P(S[k\!:])\qquad(1)

由此,我们便可定义状态集和接受状态集:

Q={qii[0,P]}A={qP}\begin{align*} Q&=\{q_i|i\in[0,|P|]\}\\ A&=\{q_{|P|}\}\\ \end{align*}

(1)(1) 可推知:

#ϕ(S[k ⁣:]c)=σP(S[k ⁣:]c)#δ(ϕ(S[k ⁣:]),c)=σP(S[k ⁣:]c)#δ(σP(S[k ⁣:]),c)=σP(S[k ⁣:]c)\begin{align*} ^\#\phi(S[k\!:]c)&=\sigma_P(S[k\!:]c)\\ ^\#\delta(\phi(S[k\!:]),c)&=\sigma_P(S[k\!:]c)\\ ^\#\delta(\sigma_P(S[k\!:]),c)&=\sigma_P(S[k\!:]c)\\ \end{align*}

我们有:

σP(P[σP(S[k ⁣:]) ⁣:]c)=σP(S[k ⁣:]c)\begin{align*} \sigma_P(P[\sigma_P(S[k\!:])\!:]c)&=\sigma_P(S[k\!:]c)\\ \end{align*}

图形化证明:

P: ⁣c ⁣ ⁣c ⁣ ⁣c ⁣P[:σ] ⁣c ⁣ ⁣c ⁣ ⁣c ⁣S: ⁣c ⁣ ⁣c ⁣ ⁣c ⁣ ⁣c ⁣ ⁣c ⁣S[k:][:σ]S[k:] ⁣c ⁣P: \overbrace{ \fcolorbox{grey}{black}{\!\phantom{c}\!} \fcolorbox{grey}{black}{\!\phantom{c}\!} \fcolorbox{grey}{black}{\!\phantom{c}\!} }^{P[:\sigma]} \fcolorbox{b}{white}{\!c\!} \fcolorbox{grey}{white}{\!\phantom{c}\!} \dots \fcolorbox{grey}{white}{\!\phantom{c}\!} \quad S:\underbrace{ \fcolorbox{grey}{white}{\!\phantom{c}\!} \dots \fcolorbox{grey}{white}{\!\phantom{c}\!} \overbrace{ \fcolorbox{grey}{black}{\!\phantom{c}\!} \fcolorbox{grey}{black}{\!\phantom{c}\!} \fcolorbox{grey}{black}{\!\phantom{c}\!} }^{S[k:][:\sigma]} }_{S[k:]} \fcolorbox{b}{white}{\!c\!}

黑色部分为追加字符前的 σP\sigma_P 相等部分(见定义)。

追加字符后,σP(S[k ⁣:]c)\sigma_P(S[k\!:]c) 的取值不会超出黑色部分的左侧,否则 σP(S[k ⁣:])\sigma_P(S[k\!:]) 会有更优解(即黑色部分可以更长),与定义矛盾。

由此,我们定义状态转移函数:

δ(q,c):=σP(P[q ⁣:]c)\delta(q,c):=\sigma_P(P[q\!:]c)

其取值表可以通过预处理得出。

引理 11σP(Sc)σP(S)+1\sigma_P(Sc)\le \sigma_P(S)+1

引理 22

P:Σ,k:N,c:Σ,σP(Sc)=σP(P[σP(S) ⁣:]c)\forall P:\Sigma^*,k:\N,c:\Sigma,\\ \sigma_P(Sc)=\sigma_P(P[\sigma_P(S)\!:]c)

q:=σP(S)r:=σP(Sc)q:=\sigma_P(S)\quad r:=\sigma_P(Sc),即目标变为:

r=σP(P[q:]c)r=\sigma_P(P[q:]c)

由定义,P[q ⁣:]=S[: ⁣q]P[q\!:]=S[:\! q],即 P[q ⁣:]SP[q\!:]\sqsupset S

由后缀的传递性,P[q ⁣:]cSc(a)P[q\!:]c\sqsupset Sc\qquad(\rm{a})

由定义,P[r ⁣:]=Sc[: ⁣r]P[r\!:]=Sc[:\! r],即 P[r ⁣:]ScP[r\!:]\sqsupset Sc

由引理 11rq+1r\le q+1,即 P[r ⁣:]P[q ⁣:]c|P[r\!:]|\le|P[q\!:]c|,由后缀重合定理,前述两者均为 ScSc 的后缀,因此能推出 P[r ⁣:]P[q ⁣:]cP[r\!:]\sqsupset P[q\!:]c

由后缀函数的定义:S1S2σP(S1)σP(S2)S_1\sqsupset S_2\Rarr \sigma_P(S_1)\le\sigma_P(S_2)

因此,σP(P[r ⁣:])σP(P[q ⁣:]c)\sigma_P(P[r\!:])\le\sigma_P(P[q\!:]c),即 rσP(P[q ⁣:]c)r\le\sigma_P(P[q\!:]c) 也因此,由 (a)(\rm{a})σP(P[q ⁣:]c)σP(Sc)\sigma_P(P[q\!:]c)\le\sigma_P(Sc),即 σP(P[q ⁣:]c)r\sigma_P(P[q\!:]c)\le r

即,r=σP(P[q ⁣:]c)r=\sigma_P(P[q\!:]c),引理得证。