KMP

创建日期:2025-01-09


KMP基本原理和实现

KMP的作用是在一个字符串 \(s\) 中查找另一个字符串 \(t\),我们\(s\) 称为文本串,\(t\) 称为模式串

不妨令 \(n=|s|,m=|t|\)

在朴素算法中,我们需要 \(O(n)\) 枚举模式串在文本串中的起始位置 \(i\),再 \(O(m)\) 检查模式串的每一个字符 \(t_j\) 是否与文本串 \(s_j\) 相比配。

可以发现,在检查过程中,每当有一位匹配不上时,就可以跳过并枚举检查下一个模式串出现位置,故常规情况下,算法复杂度到不了 \(O(n \times m)\)。但即便如此,在精心构造的数据下,仍能使算法复杂度达到 \(O(n \times m)\)

例如以下数据:
\(s=aaaaaaaaaaaaab\)
\(t=aaaaab\)

在匹配过程中,每次都会检查到模式串的最后一位,且几乎每次都会匹配失败,故复杂度达到 \(O(n \times m)\)

可以发现,复杂度如此高的原因是在每次失败后 \(j\) 直接回到了1,浪费了大量重复时间。正是因为如此,才需要KMP。

KMP的步骤如下:

设置指针 \(i\) 与指针 \(j\)\(i\) 表示现在正在检查 \(s\) 中的第 \(i\) 位与 \(t\) 中的第 \(j\) 位。

  • \(s_i\) 等于 \(t_j\) 时,将 \(i\)++, \(j\)++,继续检查。
  • \(s_i\) 不等于 \(t_j\) 时,将 \(j\) 赋值为 \(t\)\(j\) 结尾的border的长度(字符串下表从0开始)。

那border长度数组怎么求呢?

令以 \(k\) 结尾的border长度为 \(nxt_k\)

设置指针 \(i\)\(j\) 表示正在检查 \(t\) 中以 \(i\) 结尾的后缀与以 \(j\) 结尾的前缀

  • \(s_i\) 等于 \(s_j\) 时,将 \(i\)++,\(j\)++并将 \(nxt_j\)设为 \(j\),继续检查。
  • \(s_i\) 不等于 \(s_j\) 时,将 \(j\) 赋值为 \(nxt_j\)

这个过程与之前匹配的过程很相似,代码也差不多。

模版代码如下:

#include <bits/stdc++.h>
using namespace std;

const int LEN = 1e6 + 50;
string s, t;
int nxt[LEN];

void get_next() {
	nxt[0] = -1;
	int i = 0, j = -1;
	
	while (i < t.size()) {
		if (j == -1 || t[i] == t[j])
			nxt[++i] = ++j;
		else
			j = nxt[j];
	}
} 

void KMP() {
	get_next();
	int i = 0, j = 0;
	
	while (i < s.size()) {
		if (j == t.size() - 1 && s[i] == t[j]) {
			cout << i - t.size() + 2 << '\n';
			j = nxt[j];
		}
		if (j == -1 || s[i] == t[j])
			i++, j++;
		else
			j = nxt[j];
	}
}

int main() {
	cin >> s >> t;
	KMP();
	for (int i = 0; i < t.size(); i++)
		cout << nxt[i + 1] << ' ';
	return 0;
}

KMP常用的结论

1.最短周期

题目概述:见洛谷P4391

结论:最短周期长度等于 \(|s|-nxt_{|s|}\)

2.最长周期

题目概述:见洛谷P3435

结论:

\[f(x)=nxt_x x \le 0 ? x : nxt_x \]

则最长周期长度等于 \(|s|-f(x)\)

注意:在实际编写代码的过程中,可以再 \(f(x)\) 函数中使用类似并查集的路径压缩优化。

posted @ 2026-05-19 11:47  xubaichuan  阅读(5)  评论(0)    收藏  举报