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\) 中的所有出现了。

posted @ 2025-10-31 10:50  LTC_Augenstern  阅读(2)  评论(0)    收藏  举报