KMP 字符串匹配及基础应用

一些闲话

CSP 考前一个多月,发现自己字符串学的都忘完了,赶紧来开坑。

基本算法(板题 洛谷 P3375

KMP(Knuth Morris Pratt)字符串匹配算法:在 \(\mathcal O(n+m)\) 的时间内,在原串 \(S\) 内查找为 \(P\) 的子串。

算法流程分为两部分:1. 预处理并计算 \(next\) 数组;2. 利用上一步的结果进行字符串匹配。

下面约定 \(n=|P|, m=|S|\)

next 数组的定义

对于模式串 \(P\)\(P_{1 \sim i}\) 的前缀,定义 \(next_i\) 为满足 \(P_{1 \sim k} = P_{(i-k+1) \sim i}\) 的最大 \(k(k < i)\) 的值。

即找出 \(P_{1 \sim i}\)最长真前缀使得它等于 \(P_{1 \sim i}\) 的一段等长的后缀。

注:\(next\) 数组,国外文献中一般称为 LPS Table,即 Longest Prefix which is also Suffix,可以对照其英文理解。

next 数组有什么用(如何利用 next 来匹配)

假如我们已经求出了 \(next_1 \sim next_n\),考虑利用其进行子串匹配。

对于 \(S\) 中的每一个位置 \(i\),令变量 \(j\) 表示模式串 \(P\) 已经匹配完了 \(j\) 个字符。

其初值表示上次已经匹配到了 \(P_j\) 的位置,即以 \(S_{i-1}\) 结尾的一段长度为 \(j\) 的后缀与 \(P_{1 \sim j}\) 是匹配的。

现在我们检查 \(P_{j+1}\) 是否与 \(S_i\) 匹配。(步骤 A)

  • 如果相等,那么 \(S\)\(P\) 又往后多匹配了一位,转步骤 B。

  • 如果不等,朴素的想法是将 \(j\) 回到 \(1\) 重新匹配;但由 \(next\) 数组的定义,我们不妨令 \(j=next_j\)(见下图),然后回到步骤 A。

接着让 \(i\)\(j\) 分别 \(+1\),继续匹配下一个字符。(步骤 B)

如果在某一时刻有 \(j=n\),那么说明 \(S_{(i-n+1) \sim i} = P\),匹配成功。令 \(j=next_j\) 继续匹配。

可以证明,时间复杂度为 \(\mathcal O(m)\)

参考代码(C++):

for (int i = 1, j = 0; i <= m; ++i) {
	while (j && S[i] != P[j + 1]) j = nxt[j];
	if (S[i] == P[j + 1]) j++;
	if (j == n) {
		printf("Found pattern! Begins at: %d\n", i - n);
		j = nxt[j];
	}
}

如何高效地求 next 数组

对于 \(P\) 中的位置 \(i\),假设我们已经求出 \(1 \sim i-1\)\(next\),考虑如何求 \(next_i\)

同样令变量 \(j\) 初值为 \(next_{i-1}\)

比较 \(P_i\)\(P_{j+1}\),分两种情况讨论:

  • 如果 \(P_i = P_{j+1}\),说明 \(P_{1 \sim i}\) 中长度为 \(j\) 的前缀向后扩展一位后,仍然等于其同样长的后缀。这符合 \(next\) 的定义,因此 \(next_i = j+1\)

  • 否则,令 \(j=next_j\),重新比较(原理见下图)。

同样可以证明,上述操作是 \(\mathcal O(n)\) 的。

参考代码(C++):

// 注意由于 next[i] < i,所以 next[1] = 0,i 从 2 开始
for (int i = 2, j = 0; i <= n; ++i) {
	while (j && P[i] != P[j + 1]) j = nxt[j];
	if (P[i] == P[j + 1]) j++;
	nxt[i] = j;
}

将二者放在一起,即可在 \(\mathcal O(n+m)\) 的时间复杂度内解决子串匹配问题。

至于复杂度证明…… 由于作者不会证这玩意儿,这里就不展开了。

(感性理解一下,这个基于双指针的算法,跳 \(next\) 跳起来是很快的,每次均摊是常数级别?反正是比暴力匹配快很多)

如果你背不出 KMP 板子

这里有一个解决方案

当然是写字符串哈希了,如果你不放心就取多个模数好了~

但是遇到下面的应用,而不是裸的匹配题,字符串哈希就很难有用武之地了,所以建议还是把板子记下来。

(查了一下,利用字符串哈希做子串匹配的方法,在国外居然还有个名字叫 Rabin-Karp 算法?)

循环节问题(板题 洛谷 P4391

待补充

哦对了,还有一道最长循环节问题,可以留作思考~

带删的字符串匹配(板题 洛谷 P4824

待补充

Z 函数 / 扩展 KMP(板题 洛谷 P5410

待补充

一些待完成的习题

NOIP2020 字符串匹配

NOI2014 动物园

更多请参见 OI-wiki

posted @   Greenzhe_awa  阅读(36)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示