前置知识

前缀 是指从串首开始到某个位置 \(i\) 结束的一个特殊子串.

真前缀 指除了 \(S\) 本身的 \(S\) 的前缀.

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

后缀 是指从某个位置 \(i\) 开始到整个串末尾结束的一个特殊子串.

真后缀 指除了 \(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 \left [i \right ] = \max_{k = 0 \sim i} \{s \left[0 \sim k - 1 \right ] = s \left[i - \left(k - 1 \right) \sim i \right]\}
\]

特别地, 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.

\[\underbrace{\overbrace{s_0 ~ s_1 ~ s_2}^{nxt[i] = 3} ~ s_3}_{nxt[i+1] = 4} ~ \dots ~ \underbrace{\overbrace{s_{i-2} ~ s_{i-1} ~ s_{i}}^{nxt[i] = 3} ~ s_{i+1}}_{nxt[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]

\[\overbrace{\underbrace{s_0 ~ s_1}_j ~ s_2 ~ s_3}^{nxt[i]} ~ \dots ~
\overbrace{s_{i-3} ~ s_{i-2} ~ \underbrace{s_{i-1} ~ s_{i}}_j}^{nxt[i]} ~ s_{i+1}
\]

如果我们找到了这样的长度 \(j\), 那么仅需要再次比较 s[i + 1]s[j]. 如果它们相等, 那么就有 nxt[i + 1] = j + 1. 否则, 我们需要找到子串 s[0 ~ i] 仅次于 \(j\) 的第二长度 \(j_{2}\), 使得前缀性质得以保持, 如此反复, 直到 \(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\), 有这样的性质:

\[\overbrace{\underbrace{s_0 ~ s_1}_j ~ s_2 ~ \underbrace{s_3 ~ s_4}_j}^{nxt[i]} ~ \dots ~
\overbrace{s_{i-4} ~ s_{i-3} ~ s_{i-2} ~ \underbrace{s_{i-1} ~ s_{i}}_j}^{nxt[i]} ~ s_{i+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\), 我们尝试找到并展示 \(s\) 在 \(t\) 中的所有出现.

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

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

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

因此如果在某一位置 \(i\) 有 nxt[i] = n 成立, 则字符串 \(s\) 在字符串 \(t\) 的 \(i - (n - 1) - (n + 1) = i - 2n\) 处出现.

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

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

/*
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;
}

「学习笔记」KMP 算法的更多相关文章

  1. 「学习笔记」字符串基础:Hash,KMP与Trie

    「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...

  2. 「学习笔记」Min25筛

    「学习笔记」Min25筛 前言 周指导今天模拟赛五分钟秒第一题,十分钟说第二题是 \(\text{Min25}​\) 筛板子题,要不是第三题出题人数据范围给错了,周指导十五分钟就 \(\text{AK ...

  3. 「学习笔记」FFT 之优化——NTT

    目录 「学习笔记」FFT 之优化--NTT 前言 引入 快速数论变换--NTT 一些引申问题及解决方法 三模数 NTT 拆系数 FFT (MTT) 「学习笔记」FFT 之优化--NTT 前言 \(NT ...

  4. 「学习笔记」FFT 快速傅里叶变换

    目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...

  5. 「学习笔记」Treap

    「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...

  6. 「学习笔记」平衡树基础:Splay 和 Treap

    「学习笔记」平衡树基础:Splay 和 Treap 点击查看目录 目录 「学习笔记」平衡树基础:Splay 和 Treap 知识点 平衡树概述 Splay 旋转操作 Splay 操作 插入 \(x\) ...

  7. 「学习笔记」wqs二分/dp凸优化

    [学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...

  8. 「学习笔记」ST表

    问题引入 先让我们看一个简单的问题,有N个元素,Q次操作,每次操作需要求出一段区间内的最大/小值. 这就是著名的RMQ问题. RMQ问题的解法有很多,如线段树.单调队列(某些情况下).ST表等.这里主 ...

  9. 「学习笔记」递推 & 递归

    引入 假设我们想计算 \(f(x) = x!\).除了简单的 for 循环,我们也可以使用递归. 递归是什么意思呢?我们可以把 \(f(x)\) 用 \(f(x - 1)\) 表示,即 \(f(x) ...

  10. 「学习笔记」min_25筛

    前置姿势 魔力筛 其实不看也没关系 用途和限制 在\(\mathrm{O}(\frac{n^{0.75}}{\log n})\)的时间内求出一个积性函数的前缀和. 所求的函数\(\mathbf f(x ...

随机推荐

  1. 开源项目audioFlux: 针对音频领域的深度学习工具库

    目录 时频变换 频谱重排 倒谱系数 解卷积 谱特征 音乐信息检索 audioFlux是一个Python和C实现的库,提供音频领域系统.全面.多维度的特征提取与组合,结合各种深度学习网络模型,进行音频领 ...

  2. DG:windows密码文件

    问题描述:搭建DG,找不到密码文件的位置,就给备库重新生成了一个密码文件,传到了备库,但是拉到了备库以后,恢复过程中,trace日志在报错,后来才知道windows下的密码文件跟linux平台下的面文 ...

  3. 虚拟内存与malloc/new原理详解

    malloc malloc()函数并不是系统调用,而是 C 库里的函数,用于动态分配内存.malloc() 分配的是虚拟内存,而不是物理内存.如果分配后的虚拟内存没有被访问的话,是不会将虚拟内存映射到 ...

  4. 省市县树形结构打印-.netCore控制台程序

    using CityJson;using Dapper;using Newtonsoft.Json;{ using (var db = DbHelper.Db()) { //数据格式 //code_p ...

  5. 绝对强大的三大linux指令:ar, nm, objdump

    前言 如果普通编程不需要了解这些东西,如果想精确控制你的对象文件的格式或者你想查看一下文件对象里的内容以便作出某种判断,刚你可以看一下下面的工具:objdump, nm, ar.当然,本文不可能非常详 ...

  6. SSM整合之基础xml文件以及基础理解

    文章目录 前言 什么是SSM框架 实例 pom依赖 Spring 文件配置 最后 展示代码 总结 前言 最近在学习SSM框架,就去找了一些视频来看,写一篇整合的文章,文章包括一些整合的依赖,以及创建的 ...

  7. OceanBase的学习与使用

    OceanBase的学习与使用 简介 1. OceanBase数据库 注意这一块下载的其实是rpm包. 一般是通过下面的OAT或者是OCP工具进行安装. 有x86还有ARM两种架构. 虽然是el7结尾 ...

  8. 最短路(Floyed、Dijkstra、Bellman-Ford、SPFA)

    一.Floyed-Warshall算法 枚举中间点起点终点,对整个图进行松弛操作,就能得到整个图的多源最短路径: 例:POJ2240 Arbitrage Arbitrage is the use of ...

  9. 2021-08-29:N * M的棋盘(N和M是输入参数),每种颜色的格子数必须相同的,上下左右的格子算相邻,相邻格子染的颜色必须不同,所有格子必须染色,返回至少多少种颜色可以完成任务。 福大大 答

    2021-08-29:N * M的棋盘(N和M是输入参数),每种颜色的格子数必须相同的,上下左右的格子算相邻,相邻格子染的颜色必须不同,所有格子必须染色,返回至少多少种颜色可以完成任务. 福大大 答案 ...

  10. 利用简单的IO操作实现M3U8文件之间的合并

    先上代码: 1 @SneakyThrows //合并操作,最终文件不包含结束标识,方便多次合并 2 private static void mergeM3U8File(String source, S ...