KMP
概念
令 \(|s|\) 为 \(s\) 的长度。
-
子串:字符串 \(s\) 中任意的连续的一段被称为 \(s\) 的子串,特别地,空串也是 \(s\) 的子串。
-
前缀:字符串 \(s\) 中的任意一个从 \(1\) 开始的子串被称为 \(s\) 的前缀。
-
后缀:字符串 \(s\) 中的任意一个末尾为 \(|s|\) 的子串被称为 \(s\) 的后缀。
-
border:既是 \(s\) 的前缀又是 \(s\) 的后缀的字符串就是 \(s\) 的一个 border。
-
周期:令 \(p\) 为 \(s\) 的周期,那么对于 \(1 \le i \le |s| - p\),都有 \(s_i = s_{i + p}\)。
用 pre(s, k) 表示 \(s\) 的长度为 \(k\) 的前缀,suf(s, k) 表示 \(s\) 的长度为 \(k\) 的后缀。
border 与周期
当 \(s\) 的长度为 \(k\) 的前缀为 \(s\) 的 border 时,\(|s| - k\) 一定是 \(s\) 的周期。
border 的传递性
1.\(s\) 是 \(t\) 的 border,\(t\) 是 \(r\) 的 border,那么 \(s\) 是 \(r\) 的 border。
2.\(s\) 是 \(r\) 的 border,\(t \ (|t| > |s|)\) 是 \(r\) 的 border,那么 \(s\) 是 \(t\) 的 border。
所以,我们用 mb(s) 表示 \(s\) 的最长 border,那么,\(s\) 的所有 border 就分别为 mb(s),mb(mb(s)) ……。
KMP
单模匹配算法,在一个文本串 \(s\) 中查找一个模式串 \(t\),计算复杂度为 \(O(|s| + |t|)\),是此类算法可以达到的最优复杂度,空间复杂度也很优秀,为 \(O(|s|)\)。
令 \(|s| = n, |t| = m\)。
暴力
枚举 \(1 \le i \le n\),暴力往后找 \(m\) 个字符,判断是否与 \(t\) 相等,时间复杂度为 \(O(n \times m)\)。
正解
由于我们的暴力在每次碰到一个不一样的字符(也就是失配)时,就会重头开始配对,会浪费很多时间去重复判断已经判断过的字符,而 kmp 就可以很好的解决这个问题。
我们用两个指针 \(i, j\) 分别表示当前匹配到 \(s\) 中的第 \(i\) 个字符, \(t\) 中的第 \(j\) 个字符了。
我们先举个例子:
s : a b a b a b a d b a b
t : a b a b a c b
我们会在 \(i = j = 6\) 时失配,这个时候,暴力做法会回到 \(i = 2, j = 1\),但是,我们其实可以直接这样做:
s : a b a b a b a d b a b
t : a b a b a c b
因为 aba 是 ababa 的最长 border,所以我们可以直接将 \(j\) 挪到 \(4\),这样就相当于前缀需要挪到原来的某个后缀上,又要使得这个前缀和后缀相等,自然就是挪到 border 后一个啦。
失配数组(最长 border 长度)
令当前要求 pre(s, i) 的最长 border 长度,用 \(nxt_i\) 表示。
那么令 pre(s, i - 1) 的所有 border 的长度分别为 \(k_1, k_2, k_3, \dots ,k_m \ (k_1 > k_2 > k_3 > \dots > k_m)\),那么,我们就是需要找到一个最大的 \(k_j\),使得 \(s_{k_j + 1} = s_i\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
string s1, s2;
int nxt[N];
void get_fail() {
int k = 0;
nxt[1] = 0;
for (int i = 2; i < s2.size(); i++) {
while (k && s2[i] != s2[k + 1]) {
k = nxt[k];
}
k += (s2[i] == s2[k + 1]), nxt[i] = k;
}
}
void KMP() {
int n = s1.size() - 1, m = s2.size() - 1;
for (int i = 1, j = 0; i <= n; i++) {
while (j && s1[i] != s2[j + 1]) {
j = nxt[j];
}
j += (s1[i] == s2[j + 1]);
if (j == m) {
cout << i - m + 1 << '\n', j = nxt[j];
}
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> s1 >> s2, s1 = ' ' + s1, s2 = ' ' + s2;
get_fail(), KMP();
for (int i = 1; i < s2.size(); i++) {
cout << nxt[i] << ' ';
}
return 0;
}
失配树
失配树就是用 nxt 数组建成的树。
性质
在失配树上,\(u, v\) 的最近公共祖先就是 pre(s, u) 和 pre(s, v) 的最长公共 border。
\(u, v\) 的所有祖先都是 pre(s, u) 和 pre(s, v) 的公共 border。

浙公网安备 33010602011771号