「学习笔记」KMP 算法

由于新文章的做法与旧文章不同, 因此 KMP 算法仍保留旧文章, 且经过模板题测验, 新的做法明显慢于旧的做法, 但是, 新做法更好理解.

2023.8.19 旧的文章删了,因为连我自己也看不懂了 写太丑了,相信没人能看懂它的。

注意,本文的字符串都以 0 开头!当开头为 1 时处理会有一些差别,请注意一下!

前置知识#

前缀 是指从串首开始到某个位置 i 结束的一个特殊子串. 字符串 S 的以 i 结尾的前缀表示为 Prefix(S,i), 也就是 Prefix(S,i)=S[0:i]

真前缀 指除了 S 本身的 S 的前缀.

举例来说, 字符串 abcabeda 的所有前缀为 {a, ab, abc, abca, abcab, abcabe, abcabed, abcabeda}, 而它的真前缀为 {a, ab, abc, abca, abcab, abcabe, abcabed}.

后缀 是指从某个位置 i 开始到整个串末尾结束的一个特殊子串. 字符串 S 的从 i 开头的后缀表示为 Suffix(S,i), 也就是 Suffix(S,i)=S[i:|S|1].

真后缀 指除了 S 本身的 S 的后缀.

举例来说, 字符串 abcabeda 的所有后缀为 {a, da, eda, beda, abeda, cabeda, bcabeda, abcabeda}, 而它的真后缀为 {a, da, eda, beda, abeda, cabeda, bcabeda}.

前缀函数#

定义: 给定一个长度为 n 的字符串 s, 其前缀函数被定义为一个长度为 n 的数组 nxt. 其中 nxt[i] 是子串 s[0 ~ i] 最长的相等的真前缀和真后缀的长度.

用数学语言描述如下:

nxt[i]=maxk=0i{s[0k1]=s[i(k1)i]}

特别地, nxt[0] = 0, 因为不存在真前缀和真后缀.

过程#

举例来说, 对于字符串 aabaaab,

nxt[0] = 0, a 没有真前缀和真后缀.

nxt[1] = 1, aa 只有一对相等的真前缀和真后缀: a, 长度为 1.

nxt[2] = 0, aab 没有相等的真前缀和真后缀.

nxt[3] = 1, aaba 只有一对相等的真前缀和真后缀: a, 长度为 1.

nxt[4] = 2, aabaa 相等的真前缀和真后缀有 a, aa, 最长的长度为 2.

nxt[5] = 2, aabaaa 相等的真前缀和真后缀有 a, aa, 最长的长度为 2.

nxt[6] = 3, aabaaab 相等的真前缀和真后缀只有 aab, 最长的长度为 3.

暴力求法#

cin >> s1;
len1 = s1.length();
for (int i = 1; i < len1; ++ i) {
	for (int j = i; j; -- j) {
    	if (s1.substr(0, j) == s1.substr(i - (j - 1), j)) {
			nxt[i] = j;
			break ;
		}
	}
}

优化#

第一个重要的观察是 相邻的前缀函数值至多增加 1.

参照下图所示, 只需如此考虑: 当取一个尽可能大的 nxt[i + 1] 时, 必然要求新增的 s[i + 1] 也与之对应的字符匹配, 即 s[i + 1] = s[nxt[i]], 此时 s[i + 1] = s[i] + 1.

s0 s1 s2nxt[i]=3 s3nxt[i+1]=4  si2 si1 sinxt[i]=3 si+1nxt[i+1]=4

所以当移动到下一个位置时, 前缀函数的值要么增加一, 要么维持不变, 要么减少.

s[i+1] != s[nxt[i]] 时, 我们希望找到对于子串 s[0 ~ i], 仅次于 nxt[i] 的第二长度 j, 使得在位置 i 的前缀性质仍得以保持, 也即 s[0 ~ (j - 1)] = s[(i - j + 1) ~ i]

s0 s1j s2 s3nxt[i]  si3 si2 si1 sijnxt[i] si+1

如果我们找到了这样的长度 j, 那么仅需要再次比较 s[i + 1]s[j]. 如果它们相等, 那么就有 nxt[i + 1] = j + 1. 否则, 我们需要找到子串 s[0 ~ i] 仅次于 j 的第二长度 j2, 使得前缀性质得以保持, 如此反复, 直到 j=0. 如果 s[i + 1] != s[0], 则 nxt[i + 1] = 0.

观察上图可以发现, 因为 s[0 ~ nxt[i] - 1] = s[i - nxt[i] + 1 ~ i], 所以对于 s[0 ~ i] 的第二长度 j, 有这样的性质:

s0 s1j s2 s3 s4jnxt[i]  si4 si3 si2 si1 sijnxt[i] si+1

s[0 ~ j - 1] = s[i - j + 1 ~ i]= s[nxt[i] - j ~ nxt[i] - 1]
也就是说 j 等价于子串 s[nxt[i] - 1] 的前缀函数值 (你可以把上面的 i 换成 nxt[i] - 1), 即 j = nxt[nxt[i] - 1]. 同理, 次于 j 的第二长度等价于 s[j - 1] 的前缀函数值.

cin >> s1;
len1 = s1.length();
for (int i = 1; i < len1; ++ i) {
    int j = nxt[i - 1];
	while (j && s1[i] != s1[j]) {
		j = nxt[j - 1];
	}
	if (s1[i] == s1[j]) {
		++ j;
	}
	nxt[i] = j;
}

KMP 算法#

给定一个文本 t 和一个字符串 s, 我们尝试找到并展示 st 中的所有出现.

为了简便起见, 我们用 n 表示字符串 s 的长度, 用 m 表示文本 t 的长度.

我们构造一个字符串 s + # + t, 其中 # 为一个既不出现在 s 中也不出现在 t 中的分隔符.

接下来计算该字符串的前缀函数. 现在考虑该前缀函数除去最开始 n+1 个值 (即属于字符串 s 和分隔符的函数值) 后其余函数值的意义. 根据定义,nxt[i] 为右端点在 i 且同时为一个前缀的最长真子串的长度, 具体到我们的这种情况下, 其值为与 s 的前缀相同且右端点位于 i 的最长子串的长度. 由于分隔符的存在, 该长度不可能超过 n. 而如果等式 nxt[i] = n 成立, 则意味着 s 完整出现在该位置 (即其右端点位于位置 i). 注意该位置的下标是对字符串 s + # + t 而言的.

因此如果在某一位置 inxt[i] = n 成立, 则字符串 s 在字符串 ti(n1)(n+1)=i2n 处出现.

正如在前缀函数的计算中已经提到的那样, 如果我们知道前缀函数的值永远不超过一特定值, 那么我们不需要存储整个字符串以及整个前缀函数, 而只需要二者开头的一部分. 在我们这种情况下这意味着只需要存储字符串 s + # 以及相应的前缀函数值即可. 我们可以一次读入字符串 t 的一个字符并计算当前位置的前缀函数值.

因此 Knuth–Morris–Pratt 算法(简称 KMP 算法)用 On+m 的时间以及 On 的内存解决了该问题.

/*
  The code was written by yifan, and yifan is neutral!!!
 */

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

template<typename T>
inline T read() {
	T x = 0;
	bool fg = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		fg |= (ch == '-');
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = (x << 3) + (x << 1) + (ch ^ 48);
		ch = getchar();
	}
	return fg ? ~x + 1 : x;
}

const int N = 1e6 + 5;

int nxt[N << 1];
char s1[N], s2[N], cur[N << 1];

inline void get_nxt(char* s) {
	int len = strlen(s);
	for (int i = 1; i < len; ++ i) {
		int j = nxt[i - 1];
		while (j && s[i] != s[j]) {
			j = nxt[j - 1];
		}
		if (s[i] == s[j]) {
			++ j;
		}
		nxt[i] = j;
	}
}

int main() {
	cin >> s1 >> s2;
	scanf("%s%s", s1, s2);
	strcpy(cur, s2);
	strcat(cur, "#");
	strcat(cur, s1);
	get_nxt(cur);
	int l1 = strlen(s1), l2 = strlen(s2);
	for (int i = l2 + 1; i <= l1 + l2; ++ i) {
		if (nxt[i] == l2) {
			cout << i - 2 * l2 + 1 << '\n';
		}
	}
	for (int i = 0; i < l2; ++ i) {
		cout << nxt[i] << ' ';
	}
	return 0;
}

作者:yifan0305

出处:https://www.cnblogs.com/yifan0305/p/17541239.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

转载时还请标明出处哟!

posted @   yi_fan0305  阅读(250)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2022-07-10 「学习笔记」treap(大根堆)
2022-07-10 「学习笔记」treap(小根堆)
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示