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\)。前面的暴力可以被模拟成:

  1. 初始化 \(i=j=0\)
  2. 记录 \(i_0=i\)
  3. 如果 \(s_i = t_j\),匹配成功,\(i\gets i+1\)\(j\gets j+1\)
  4. 如果 \(s_i \ne t_j\),匹配失败,\(i\gets i_0\)\(j\gets0\),返回第二步。
  5. 如果 \(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\)

不压行小清新 code | 压行 code

my record

ACAM

咕咕咕。

posted @ 2023-06-22 09:45  liangbowen  阅读(24)  评论(0编辑  收藏  举报