「学习笔记」KMP 算法
前置知识
前缀 是指从串首开始到某个位置 \(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[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.
\]
所以当移动到下一个位置时, 前缀函数的值要么增加一, 要么维持不变, 要么减少.
当 s[i+1] != s[nxt[i]] 时, 我们希望找到对于子串 s[0 ~ i], 仅次于 nxt[i] 的第二长度 \(j\), 使得在位置 \(i\) 的前缀性质仍得以保持, 也即 s[0 ~ (j - 1)] = s[(i - j + 1) ~ i]:
\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{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 算法的更多相关文章
- 「学习笔记」字符串基础:Hash,KMP与Trie
「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...
- 「学习笔记」Min25筛
「学习笔记」Min25筛 前言 周指导今天模拟赛五分钟秒第一题,十分钟说第二题是 \(\text{Min25}\) 筛板子题,要不是第三题出题人数据范围给错了,周指导十五分钟就 \(\text{AK ...
- 「学习笔记」FFT 之优化——NTT
目录 「学习笔记」FFT 之优化--NTT 前言 引入 快速数论变换--NTT 一些引申问题及解决方法 三模数 NTT 拆系数 FFT (MTT) 「学习笔记」FFT 之优化--NTT 前言 \(NT ...
- 「学习笔记」FFT 快速傅里叶变换
目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...
- 「学习笔记」Treap
「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...
- 「学习笔记」平衡树基础:Splay 和 Treap
「学习笔记」平衡树基础:Splay 和 Treap 点击查看目录 目录 「学习笔记」平衡树基础:Splay 和 Treap 知识点 平衡树概述 Splay 旋转操作 Splay 操作 插入 \(x\) ...
- 「学习笔记」wqs二分/dp凸优化
[学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...
- 「学习笔记」ST表
问题引入 先让我们看一个简单的问题,有N个元素,Q次操作,每次操作需要求出一段区间内的最大/小值. 这就是著名的RMQ问题. RMQ问题的解法有很多,如线段树.单调队列(某些情况下).ST表等.这里主 ...
- 「学习笔记」递推 & 递归
引入 假设我们想计算 \(f(x) = x!\).除了简单的 for 循环,我们也可以使用递归. 递归是什么意思呢?我们可以把 \(f(x)\) 用 \(f(x - 1)\) 表示,即 \(f(x) ...
- 「学习笔记」min_25筛
前置姿势 魔力筛 其实不看也没关系 用途和限制 在\(\mathrm{O}(\frac{n^{0.75}}{\log n})\)的时间内求出一个积性函数的前缀和. 所求的函数\(\mathbf f(x ...
随机推荐
- stm32报错
1. declaration may not appear after executable statement in block 关于编译错误的小伙伴:error: #268: declaratio ...
- Yolov8离谱报错
YoloV8离谱报错 今天下午给一个研究生小姐姐跑数据集,用的是yolov8在恒源云上租的4070的GPU服务器,跑垃圾分类数据集(https://blog.csdn.net/m0_5488250 ...
- 【Vue2.x源码系列06】计算属性computed原理
上一章 Vue2异步更新和nextTick原理,我们介绍了 JavaScript 执行机制是什么?nextTick源码是如何实现的?以及Vue是如何异步更新渲染的? 本章目标 计算属性是如何实现的? ...
- DeFi4-稳定币
稳定币--稳定 是一个相对的度量指标 波动性,收益率标准差 在一个时间段内最大跌幅 Fiat,例如: 欧元.英镑的波动率为6-12% (波动本身并不能反映价格的范围 稳定币 vs 锚定币 稳定币类型 ...
- Mapstruct使用报java: Couldn't retrieve @Mapper annotation
检查代码报错 java: Couldn't retrieve @Mapper annotation jar包冲突,去掉一个Mapstructjar包.
- 还不知道怎么 Mock ,用这 6款工具!
以下是几个常用的国外可以mock测试的工具,供参考: MockServer: MockServer 是一个开源的 API mock 测试工具,提供了强大的模拟服务器和 mock 服务功能.MockSe ...
- SQL Server数据库判断最近一次的备份执行结果
1 麻烦的地方 在SQL Server的官方文档里面可以看到备份和还原的表,但是这些表里面只能找到备份成功的相关信息,无法找到备份失败的记录,比如msdb.dbo.backupset.对于一些监控系统 ...
- 网站七牛云CDN加速配置
首先进入七牛云管理平台 1.添加域名 2.添加需要加速的域名,比如我添加的是gechuang.net 3.源站配置,这里要用IP地址,访问的目录下面要有能访问测试的文件 4.缓存配置,也就是配置缓存哪 ...
- 2021-12-24:划分字母区间。 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。 力扣763。某大厂面试
2021-12-24:划分字母区间. 字符串 S 由小写字母组成.我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中.返回一个表示每个字符串片段的长度的列表. 力扣763.某大厂面试 ...
- Django4全栈进阶之路22 项目实战(三种方式开发部门管理):方式三:FBV+ModelForm+get_object_or_404
1.视图 @login_required def department_list_view(request): departments = Department.objects.all() retur ...