前缀函数的学习报告

· · 算法·理论

在字符串算法的世界中,前缀函数犹如一把精巧的钥匙,能够高效解开模式匹配、子串搜索、字符串最小周期等经典难题。无论是代码优化还是算法竞赛,理解前缀函数的思想,都能让我们在字符串处理中游刃有余。

本文是笔者对于网络上大量文章的总结和提炼,如有不严谨之处欢迎指正。参考资料的链接已经放在文末,侵权自删。

1. 前缀函数

1.1 前缀函数的定义[^i]

对于一个长度为 n 的字符串 s,其前缀函数被定义为一个长度为 n 的数组 \pi。其中 \pi[i] 为子串 s[0\dots i] 最长的、相等的真前缀与真后缀的长度,即最大公共前后缀长度(最长 Border 的长度)

用数学语言描述如下:

\pi[i] = \max_{k = 0 \dots i}\{k: s[0 \dots k - 1] = s[i - (k - 1) \dots i]\}

特别地,规定 \pi[0]=0

1.2 前缀函数的求解

假设已知 \pi[0\dots i-1],考虑 \pi[i] 的转移。

易得若 s[0\dots i-1] 中存在第 j 长度的 Border k^{(j)} ,使得 s[k^{(j)}]=s[i],则 \pi[i] 一定可以从 k^{(j)}+1 转移而来。

则问题就变成了根据前缀函数求字符串的所有 Border 问题,详见2.1 字符串的 Border,这里直接给出结论:

k^{(j)}=\pi[k^{(j-1)}-1] \left ( j>1 \right )

则伪代码如下:

k = pi[i-1]

while k>0 && s[i]!=s[k]:
    k = pi[k-1]
# 不断回溯前一个可能的最长 Border
if s[k] == s[i]:
    pi[i] = k+1

2. 应用

2.1 字符串的所有 Border

本部分其实是 Border 自身的递归性质,如果仅仅是看文字证明的话是非常难理解的,各位可以画个图、多推几个样例感受一下。

对于一个长度为 n 字符串 s,若已得最长的 Border 长度 k^{(1)}=\pi[n-1],考虑 k^{(2)} 的转移。

根据 Border 的定义

s[0\dots k^{(1)}-1] = s[n-k^{(1)}\dots n-1]

同理

s[0\dots k^{(2)}-1] = s[n-k^{(2)}\dots n-1]

由以上二者关系可得

s[0\dots k^{(2)}-1] = s[k^{(1)}-k^{(2)}\dots k^{(j)}-1]

因为 k^{(2)} 是长度上仅次于 k^{(1)} 的最长的 Border,那么 k^{(2)} 也一定是子串 s[0\dots k^{(1)}-1] 最长 Border。

所以可得 k^{(2)}=\pi[k^{(1)}-1],数学归纳后便可得到

k^{(j)}=\pi[k^{(j-1)}-1] \left ( j>1 \right )

2.2 字符串的匹配算法

在上文了解了前缀函数的相关内容后,我们不难发现将模式串和主串用一个分割符(需保证在模式串和主串中**均未出现**)连接起来,求解这个新字符串的前缀函数,若有 $\pi[i]$ 等于模式串长度则 $i$ 为模式串在主串的一个成功匹配的位置。 那为什么一定要用分隔符呢? 这里简单举一个例子,模式串 `aa`,主串 `abaaab`,直接连接为 `aaabaaab`,这时 $\pi=\{0,1,2,0,1,2,3 ,4\}$。 你会发现受到主串前缀的干扰,最长公共前后缀长度不能**永远小于等于**模式串长度,导致匹配出的位置少甚至错误。 ## 2.3 字符串的最小周期 若字符串 $s$ 由某个子串 $t$ 重复 $k$ 次构成,则 $t$ 的长度即为 $s$ 的周期。其中最小周期为所有可能的周期中最小的那一个。 那字符串的最小周期和前缀函数又有什么关系呢? 由于 $s$ 是由某个子串 $t$ 重复 $k$ 次构成,不难能观察到 $\left |t\right |=n-\pi[n-1]$。 严格证明如下,不感兴趣的可以跳过: 对任意 $0 \leq i < n - \left |t\right |$,需证明 $s[i] = s[i + \left |t\right |]$。 因为当 $i < \pi[n-1]$ 时,由 Border 定义,有: $$ s[i] = s[n - \pi[n-1] + i] = s[i + \left |t\right | ] \quad (\because \left |t\right | = n - \pi[n-1]) $$ 又因为当 $i \geq \pi[n-1]$ 时,$\left |t\right |= n - \pi[n-1]$,重复结构保证: $$ s[i] = s[i \bmod \left |t\right |] $$ 所以 $\left |t\right |$ 符合周期性。 假设存在更小周期 $\left |t\right |' < \left |t\right |$,则对应的最长 Border 长度为: $$ \pi[n-1]' = n - \left |t\right |' > n - \left |t\right | = \pi[n-1] $$ 这与 $\pi[n-1]$ 是最大 Border 矛盾。因此 $\left |t\right |$ 是最小的周期。 证毕。[^ii] 这里推荐一个题 [POI 2006 OKR-Periods of Words](https://www.luogu.com.cn/problem/P3435),相信做完后对于以上应用有更深入的了解。 ## 2.4 字符串压缩 这个没有什么好讲的,和**2.3 字符串的最小周期**相比只多了一个要满足的条件 $n \bmod n-\pi[n-1] = 0$。 # 5. 结语 笔者目前水平有限,目前仅能总结这些内容与诸位分享并交流。之所以标号是**5. 结语**是因为还有太多内容没有添加了,包括但不限于:Boyer–Moore 算法、Z 函数、Manacher 等等字符串的相关算法,当然如果添加进来也不应该叫**前缀函数的学习报告**了。 最后,感谢网络上无私分享的算法爱好者们。知识的河流因汇聚而奔涌,愿我们永远保持对算法之美的敬畏与热爱。 $$ {\huge \mathcal{海纳百川,有容乃大} } $$ --- [^i]: 此处定义引用自[前缀函数与 KMP 算法 - OI Wiki](https://oi.wiki/string/kmp) [^ii]: 证明不严谨处欢迎指正