前置知识:字符串前缀树组
前缀函数定义
给定一个长度为 $n$ 的字符串 $s$,其 前缀函数 被定义为一个长度为 $n$ 的数组 $\pi$。
其中 $\pi[i]$ 的定义是:
- 如果子串 $s[0\dots i]$ 有一对相等的真前缀与真后缀:$s[0\dots k-1]$ 和 $s[i - (k - 1) \dots i]$,那么 $\pi[i]$ 就是这个相等的真前缀(或者真后缀,因为它们相等:))的长度,也就是 $\pi[i]=k$;
- 如果不止有一对相等的,那么 $\pi[i]$ 就是其中最长的那一对的长度;
- 如果没有相等的,那么 $\pi[i]=0$。
简单来说 $\pi[i]$ 就是,子串 $s[0\dots i]$ 最长的相等的真前缀与真后缀的长度。
用数学语言描述如下:
$$
\pi[i] = \max_{k = 0 \dots i}{k: s[0 \dots k - 1] = s[i - (k - 1) \dots i]}
$$
特别地,规定 $\pi[0]=0$。
举例来说,对于字符串 abcabcd,
$\pi[0]=0$,因为 a 没有真前缀和真后缀,根据规定为 0
$\pi[1]=0$,因为 ab 无相等的真前缀和真后缀
$\pi[2]=0$,因为 abc 无相等的真前缀和真后缀
$\pi[3]=1$,因为 abca 只有一对相等的真前缀和真后缀:a,长度为 1
$\pi[4]=2$,因为 abcab 相等的真前缀和真后缀只有 ab,长度为 2
$\pi[5]=3$,因为 abcabc 相等的真前缀和真后缀只有 abc,长度为 3
$\pi[6]=0$,因为 abcabcd 无相等的真前缀和真后缀
同理可以计算字符串 aabaaab 的前缀函数为 $[0, 1, 0, 1, 2, 2, 3]$。
计算前缀函数的朴素算法
一个直接按照定义计算前缀函数的算法流程:
- 在一个循环中以 $i = 1\to n - 1$ 的顺序计算前缀函数 $\pi[i]$ 的值($\pi[0]$ 被赋值为 $0$)。
- 为了计算当前的前缀函数值 $\pi[i]$,我们令变量 $j$ 从最大的真前缀长度 $i$ 开始尝试。
- 如果当前长度下真前缀和真后缀相等,则此时长度为 $\pi[i]$,否则令 j 自减 1,继续匹配,直到 $j=0$。
- 如果 $j = 0$ 并且仍没有任何一次匹配,则置 $\pi[i] = 0$ 并移至下一个下标 $i + 1$。
具体实现如下:
1 | vector<int> prefix_function(string s) { |
注:
string substr (size_t pos = 0, size_t len = npos) const;
显见该算法的时间复杂度为 $O(n^3)$,具有很大的改进空间。
计算前缀函数的高效算法
第一个优化
第一个重要的观察是 相邻的前缀函数值至多增加 $1$。
参照下图所示,只需如此考虑:当取一个尽可能大的 $\pi[i+1]$ 时,必然要求新增的 $s[i+1]$ 也与之对应的字符匹配,即 $s[i+1]=s[\pi[i]]$, 此时 $\pi[i+1] = \pi[i]+1$。
$\underbrace{\overbrace{s_0 ~ s_1 ~ s_2}^{\pi[i] = 3} ~ s_3}{\pi[i+1] = 4} ~ \dots ~ \underbrace{\overbrace{s{i-2} ~ s_{i-1} ~ s_{i}}^{\pi[i] = 3} ~ s_{i+1}}_{\pi[i+1] = 4}
$
所以当移动到下一个位置时,前缀函数的值要么增加一,要么维持不变,要么减少。
此时的改进的算法为:
1 | vector<int> prefix_function(string s) { |
在这个初步改进的算法中,在计算每个 $\pi[i]$ 时,最好的情况是第一次字符串比较就完成了匹配,也就是说基础的字符串比较次数是 n-1 次。
而由于存在 j = pi[i-1]+1(pi[0]=0)对于最大字符串比较次数的限制,可以看出每次只有在最好情况才会为字符串比较次数的上限积累 1,而每次超过一次的字符串比较消耗的是之后次数的增长空间。
由此我们可以得出字符串比较次数最多的一种情况:至少 1 次字符串比较次数的消耗和最多 n-2 次比较次数的积累,此时字符串比较次数为 n-1 + n-2 = 2n-3。
可见经过此次优化,计算前缀函数只需要进行 $O(n)$ 次字符串比较,总复杂度降为了 $O(n^2)$。
第二个优化
在第一个优化中,我们讨论了计算 $\pi[i+1]$ 时的最好情况:$s[i+1]=s[\pi[i]]$,此时 $\pi[i+1] = \pi[i]+1$。现在让我们沿着这个思路走得更远一点:讨论当 $s[i+1] \neq s[\pi[i]]$ 时如何跳转。
失配时,我们希望找到对于子串 $s[0\dots i]$,仅次于 $\pi[i]$ 的第二长度 $j$,使得在位置 $i$ 的前缀性质仍得以保持,也即 $s[0 \dots j - 1] = s[i - j + 1 \dots i]$:
$$
\overbrace{\underbrace{s_0 ~ s_1}j ~ s_2 ~ s_3}^{\pi[i]} ~ \dots ~ \overbrace{s{i-3} ~ s_{i-2} ~ \underbrace{s_{i-1} ~ s_{i}}j}^{\pi[i]} ~ s{i+1}
$$
如果我们找到了这样的长度 $j$,那么仅需要再次比较 $s[i + 1]$ 和 $s[j]$。如果它们相等,那么就有 $\pi[i + 1] = j + 1$。否则,我们需要找到子串 $s[0\dots i]$ 仅次于 $j$ 的第二长度 $j^{(2)}$,使得前缀性质得以保持,如此反复,直到 $j = 0$。如果 $s[i + 1] \neq s[0]$,则 $\pi[i + 1] = 0$。
观察上图可以发现,因为 $s[0\dots \pi[i]-1] = s[i-\pi[i]+1\dots i]$,所以对于 $s[0\dots i]$ 的第二长度 $j$,有这样的性质:
$$
s[0 \dots j - 1] = s[i - j + 1 \dots i]= s[\pi[i]-j\dots \pi[i]-1]
$$
也就是说 $j$ 等价于子串 $s[\pi[i]-1]$ 的前缀函数值,即 $j=\pi[\pi[i]-1]$。同理,次于 $j$ 的第二长度等价于 $s[j-1]$ 的前缀函数值,$j^{(2)}=\pi[j-1]$
显然我们可以得到一个关于 $j$ 的状态转移方程:$j^{(n)}=\pi[j^{(n-1)}-1], \ \ (j^{(n-1)}>0)$
最终算法
所以最终我们可以构建一个不需要进行任何字符串比较,并且只进行 $O(n)$ 次操作的算法。
而且该算法的实现出人意料的短且直观:
1 | vector<int> prefix_function(string s) { |
这是一个 在线 算法,即其当数据到达时处理它——举例来说,你可以一个字符一个字符的读取字符串,立即处理它们以计算出每个字符的前缀函数值。该算法仍然需要存储字符串本身以及先前计算过的前缀函数值,但如果我们已经预先知道该字符串前缀函数的最大可能取值 $M$,那么我们仅需要存储该字符串的前 $M + 1$ 个字符以及对应的前缀函数值。
KMP算法
首先,默认大家字符串匹配的暴力算法(称作Brute-Force)是已经会了的。
它最坏的情况如下图所示:
我们很难降低字符串比较的复杂度(因为比较两个字符串,真的只能逐个比较字符)。因此,我们考虑降低比较的趟数。如果比较的趟数能降到足够低,那么总的复杂度也将会下降很多。 要优化一个算法,首先要回答的问题是“我手上有什么信息?” 我们手上的信息是否足够、是否有效,决定了我们能把算法优化到何种程度。请记住:尽可能利用残余的信息,是KMP算法的思想所在。
在 Brute-Force 中,如果从 $S[i]$ 开始的那一趟比较失败了,算法会直接开始尝试从 $S[i+1]$ 开始比较。这种行为,属于典型的“没有从之前的错误中学到东西”。我们应当注意到,一次失败的匹配,会给我们提供宝贵的信息——如果 $S[i : i+len(P)]$ 与 $P$ 的匹配是在第 r 个位置失败的,那么从 $S[i]$ 开始的 (r-1) 个连续字符,一定与 P 的前 (r-1) 个字符一模一样!

需要实现的任务是“字符串匹配”,而每一次失败都会给我们换来一些信息——能告诉我们,主串的某一个子串等于模式串的某一个前缀。但是这又有什么用呢?
跳过不可能成功的字符串比较
有些趟字符串比较是有可能会成功的;有些则毫无可能。我们刚刚提到过,优化 Brute-Force 的路线是“尽量减少比较的趟数”,而如果我们跳过那些绝不可能成功的字符串比较,则可以希望复杂度降低到能接受的范围。
那么,哪些字符串比较是不可能成功的?来看一个例子。已知信息如下:
模式串 P = “abcabd”.
和主串从$S[0]$开始匹配时,在 $P[5]$ 处失配。

首先,利用上一节的结论。既然是在 P[5] 失配的,那么说明 S[0:5] 等于 P[0:5],即”abcab”. 现在我们来考虑:从 S[1]、S[2]、S[3] 开始的匹配尝试,有没有可能成功?
从 S[1] 开始肯定没办法成功,因为 S[1] = P[1] = ‘b’,和 P[0] 并不相等。从 S[2] 开始也是没戏的,因为 S[2] = P[2] = ‘c’,并不等于P[0]. 但是从 S[3] 开始是有可能成功的——至少按照已知的信息,我们推不出矛盾。

next数组
next数组是对于模式串而言的。P 的 next 数组定义为:next[i] 表示 P[0] ~ P[i] 这一个子串,使得 前k个字符恰等于后k个字符 的最大的k. 特别地,k不能取i+1(因为这个子串一共才 i+1 个字符,自己肯定与自己相等,就没有意义了)。

上图给出了一个例子。P=”abcabd”时,next[4]=2,这是因为P[0] ~ P[4] 这个子串是”abcab”,前两个字符与后两个字符相等,因此next[4]取2. 而next[5]=0,是因为”abcabd”找不到前缀与后缀相同,因此只能取0.
如果把模式串视为一把标尺,在主串上移动,那么 Brute-Force 就是每次失配之后只右移一位;改进算法则是每次失配之后,移很多位,跳过那些不可能匹配成功的位置。但是该如何确定要移多少位呢?

在 S[0] 尝试匹配,失配于 S[3] <=> P[3] 之后,我们直接把模式串往右移了两位,让 S[3] 对准 P[1]. 接着继续匹配,失配于 S[8] <=> P[6], 接下来我们把 P 往右平移了三位,把 S[8] 对准 P[3]. 此后继续匹配直到成功。
我们应该如何移动这把标尺?很明显,如图中蓝色箭头所示,旧的后缀要与新的前缀一致(如果不一致,那就肯定没法匹配上了)!
回忆next数组的性质:P[0] 到 P[i] 这一段子串中,前next[i]个字符与后next[i]个字符一模一样。既然如此,如果失配在 P[r], 那么P[0]~P[r-1]这一段里面,前next[r-1]个字符恰好和后next[r-1]个字符相等——也就是说,我们可以拿长度为 next[r-1] 的那一段前缀,来顶替当前后缀的位置,让匹配继续下去!
您可以验证一下上面的匹配例子:P[3]失配后,把P[next[3-1]]也就是P[1]对准了主串刚刚失配的那一位;P[6]失配后,把P[next[6-1]]也就是P[3]对准了主串刚刚失配的那一位。

如上图所示,绿色部分是成功匹配,失配于红色部分。深绿色手绘线条标出了相等的前缀和后缀,其长度为next[右端]. 由于手绘线条部分的字符是一样的,所以直接把前面那条移到后面那条的位置。因此说,next数组为我们如何移动标尺提供了依据。接下来,我们实现这个优化的算法。
而next数组的求法,我们前面已经讲过了,这里就不在赘述。
小练习
- UVA 455 “Periodic Strings”
- UVA 11022 “String Factoring”
- UVA 11452 “Dancing the Cheeky-Cheeky”
- UVA 12604 - Caesar Cipher
- UVA 12467 - Secret Word
- UVA 11019 - Matrix Matcher
- SPOJ - Pattern Find
- Codeforces - Anthem of Berland
- Codeforces - MUH and Cube Walls
好耶ヾ(✿゚▽゚)ノ!!!





