KMP
定义
真前/后缀:原串的一个不等于原串的前/后缀。
给定字符串 \(S\),定义 \(S\) 的一个 \(border\) 为: \(S\) 的一个真前缀,满足它同时为 \(S\) 的一个真后缀。
也就是说,\(S\) 的一个 \(border\) 同时是 \(S\) 的一个真前缀和一个真后缀。显然这个真前缀和真后缀的长度相等。
O(n) 求 border 数组
定义
在本段中,定义 \(str_i\) 为串 \(S\) 长度为 \(i\) 的前缀,\(f_i\) 为 \(str_i\) 的最长 \(border\),\(S_i\) 为 \(S\) 的第 \(i\) 个字符。
为了方便,我们置 \(f_0=0\)。
算法
使用递推法。也就是说,考虑我们已经知道了 \(f_{1\cdots i-1}\),现在我们要求 \(f_i\)。
性质
设串 \(S\) 的最长 \(border\) 为 \(S'\),\(S'\) 的最长 \(border\) 为 \(S''\)。
则:\(S''\) 为 \(S\) 的一个 \(border\),并且是 \(S\) 第二长的 \(border\)。
证明:因为 \(S'\) 是 \(S\) 的 \(border\),设 \(S\) 的结构为 \(S'+T+S'\) (其中 \(T\) 是多出来的一段)。
同理,\(S'\) 的结构设为 \(S''+T'+S''\)。
则 \(S\) 的结构可以设为 \(S''+T'+S''+T+S''+T'+S''\)(代入求得),所以 \(S''\) 是 \(S\) 的一个 \(border\)。
又因为 \(S'\) 是 \(S\) 的最长 \(border\),\(S''\) 是 \(S'\) 的最长 \(border\),所以 \(S''\) 是 \(S\) 的第二长 \(border\)。
如何递推
我们知道了 \(f_{1\cdots i-1}\)。
对于 \(str_i\) 的一个 \(border\),如果它长度 \(>1\),那么它一定是由一个 \(str_{i-1}\) 的 \(border\) 加上一个字母得到。
所以我们只需要从大到小枚举 \(str_{i-1}\) 的所有 \(border\),然后尝试往后匹配 \(1\) 个字母即可。
如果失败了,就枚举下一个;否则,我们就得到了 \(f_i\)。\(f_i\) 的计算方式见代码。
如果全部枚举完还没有答案,那么如果 \(S_i=S_1\),\(f_i=1\);否则 \(f_i=0\)。
如何枚举:根据上面的性质,注意到 \(f_{i-1},f_{f_{i-1}},f_{f_{f_{i-1}}}\),这样枚举下去,就可以枚举出 \(str_{i-1}\) 的所有 \(border\),直到这个 \(border\) 长度为 \(1\)。
这样的复杂度就是 \(O(n)\) 的。代码:\((oi-wiki)\)
vector<int> prefix_function(string s) {
int n = (int)s.length();
vector <int> f(n);
for (int i = 1; i < n; i++) {
int j = f[i - 1];
while (j > 0 && s[i] != s[j]) j = f[j - 1];//枚举前缀 i-1 的所有 border 并尝试匹配
if (s[i] == s[j]) j++;//如果匹配成功,fi=最大的匹配成功的 border 的长度 +1;如果失败,则这段代码就变为判断 si 和 s0 是否相同
f[i] = j;
}
return pi;
}
复杂度证明可以用势能分析一类的东西。
kmp
我们已经会算出所有 \(f_i\) 了。如何匹配两个字符串?
设:在 \(S\) 中寻找 \(T\) 中的所有出现。
构造字符串 \(P=S+\) (一个 \(S,T\) 中均不会出现的字符) \(+T\)。
然后求这个串的 \(f\),如果某个 \(f_i=size(S)\),那么说明有一个前缀的 \(border\) 长度为 \(size(S)\),这意味着我们找到了一个在 \(T\) 中出现的 \(S\)。
这样我们就可以找到所有 \(S\) 在 \(T\) 中的所有出现了。

浙公网安备 33010602011771号