KMP 学习笔记
$$\Huge\color{red}\S\text{更好的阅读体验}\S$$
前言
KMP 并不困难,刚刚理解好了,来写篇学习笔记。
到时候可能会把 ACAM 的也补上。
KMP
作用
KMP 是一个单模匹配算法,即:在字符串 \(s\) 里查找一个字符子串 \(t\)。
KMP 全称 Knuth-Morris-Pratt,这是三个人,他们共同提出了这个算法。
时间复杂度 \(O(|s| + |t|)\),和输入同阶,显然是最优复杂度。
它强大的地方在于,不仅效率极高,代码也非常精辟!
\(\color{red}\textbf{本文所有字符串的下标都默认从 0 开始}\)。
☑ 要点 \(1\):避免回退
暴力
遍历 \(s\) 的每一位作为起点,然后看这一段能不能匹配上,配不上就滚蛋,下一个。
把图中画 \(\text{X}\) 的地方称作失配点。
注意到 \((2)(3)(4)\) 都一下就寄了,但是接近匹配的 \((1)\) 就匹配了很长时间。这意味着,只要 \(s,t\) 的重复字母足够多,就会被卡成 \(O(|s| \times |t|)\)。
为了方便 KMP 的理解,想象 \(s\) 上有一个指针 \(i\),\(t\) 上有一个指针 \(j\)。前面的暴力可以被模拟成:
- 初始化 \(i=j=0\)。
- 记录 \(i_0=i\)。
- 如果 \(s_i = t_j\),匹配成功,\(i\gets i+1\),\(j\gets j+1\)。
- 如果 \(s_i \ne t_j\),匹配失败,\(i\gets i_0\),\(j\gets0\),返回第二步。
- 如果 \(j\) 已经到末尾了,匹配成功!
优化
我们发现,\((1)\) 那里几乎遍历完了 \(t\),可是这一无是处。
\(\color{red}\textbf{KMP 的精髓:}i\textbf{ 永远不回退!}\)
进行一些分析:
- \(t\) 失配点之前,每个字母互不相同。\(s\) 失配点以前的点都不能作为起点,也就是说,\(i\) 可以直接跳到失配点去。
- 显然,这种情况建立在起点能匹配上的前提下。
- 第一个字母相同,其他的却不同,说明这些字母都得寄。
- 故直接冲到失配点即可。
- \(t\) 失配点之前,前后缀相同。直接跳到后缀那里去即可。难理解看图。
- 其他情况。\(j\) 跳回 \(0\),\(i\) 不变。
☑ 要点 \(2\):快速计算前后缀匹配
在前文,KMP 的大致原理我们已经了解。唯一的问题是,如何快速计算
\(t\) 失配点之前,最长的前后缀是多少?
假设 \(\text{next}_i\) 表示在 \(t:[0,i]\) 中,前缀与后缀相同的最长长度。比如 \(\color{red}\text{abc}\color{black}\text{ed}\color{red}\text{abc}\) 的 \(\text{next}\) 就是 \(3\)。
注意这个 \(\text{next}_i\) 只和 \(t\) 有关。类似于之前的分析。
- 假定 \(j = \text{next}_i\)。
- 若 \(t_i = t_j\),则 \(\text{next}_{i+1}=\text{next}_i+1\),即 \(j+1\)。
- 若 \(t_i \ne t_j\),前面的 \(\text{next}_{i}\) 将不能延续到 \(\text{next}_{i+1}\),必须删减。
- 持续进行 \(j \gets \text{next}_i\),直到 \(t_i = t_j\)。
- 此时 \(\text{next}_{i+1}\gets j+1\) 即可。
第一种情况和第二种情况是可以合并的,也就是说这个前后缀长度,可以是 \(0\)。
细节 + 代码实现
综上所述,KMP 其实非常简单,只要搞懂这两个要点,代码实现非常容易。
求 \(\text{next}_i\)
int nxt[N];
void getnxt(string s) {
int n = s.length();
for (int i = 1, j = 0; i < n; i++) {
while (j && s[i] != s[j]) j = nxt[j];
if (s[i] == s[j]) nxt[i + 1] = ++j; else nxt[i + 1] = 0;
}
}
单模匹配
void match(string s, string t) {
int n = s.length(), Tlen = t.length();
for (int i = 0, j = 0; i < n; i++) {
while (j && s[i] != t[j]) j = nxt[j];
if (s[i] == t[j]) j++;
if (j == Tlen) {/*print answer*/}
}
}
模板题
提交地址。题目不仅要单模匹配,还要输出 \(\text{next}_i\)。
ACAM
咕咕咕。