[学习笔记] KMP算法——烤馍片(超详细)
1. KMP简介
kmp算法,是一种线性字符串匹配(父子串为 root,子子串为 s),由 D.E.Knuth,J.H.Morris 和 V.R.Pratt 提出的,因此人们称它为KMP算法。
2. 暴力思路
举个样例吧。
| 下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| root | a | b | c | a | a | b | c | a | b |
| s | a | b | c | a | b |
拿到这个串,我们让两个指针 \(l,r\) 分别指向 \(root\) 和 \(s\)。
共 \(len(root)\) 轮,对于第 \(i\) 轮,让 \(l\gets i,r\gets 1\),并执行按顺序执行以下操作:
- 如果 \(r=len(s)\) 则找到 \(s\)。
- 如果 \(r\ne len(s)\) 且 \(root_l=s_r\),则 \(l\gets l+1,r\gets r+1\),并重复该操作。
- 如果 \(root_l\ne s_r\),则跳过该阶段。
就拿样例而言,对于第 \(1\) 轮,\(l=1,r=1\),发现 \(root_1=s_1\),则 \(l+1,r+1\)。当 \(l=5,r=5\) 时,\(root_5\ne s_5\),则进行第2轮,\(l=2,r=1\)......
你会发现,一旦中途不符合条件,已知的信息最会浪费。那有没有不浪费已知信息的算法呢?
答案是有的,那就是烤馍片算法。
3. KMP算法
3.1 过程
再来举个样例:
| 下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| root | A | B | C | D | A | B | C | D | A | B | C | E |
| s | A | B | C | D | A | B | C | E |
我们匹配好前面八位的时候,发现第九位不对,我们就移动 \(s\),根据前面已知的,我们就可以像下面这样:
| 下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| root | A | B | C | D | A | B | C | D | A | B | C | E |
| s | A | B | C | D | A | B | C | E |
直接向后移动四位,然后我们的 \(l\) 继续从第五位开始,直到匹配成功。
注意:我们都是判断 \(l\) 和 \(r\) 下一个位置是否为相同。
前缀串与后缀串
如 abcde:
前缀串:a,ab,abc,abcd,abcde
后缀串:e,de,cde,bcde,abcde
其实,\(r\) 只是回跳到使得 \(root\) 前 \(l\) 段的后缀与 \(s\) 前 \(r\) 段的前缀相等的最长长度的位置(除了自己),有点长,大家可以分开理解。
例如,当 \(l=8,r=8\) 时,下一个位置不一样,则将 \(r\) 回退到 \(3\),因为 \(root\) 前 \(8\) 个中的倒数 \(3\) 个与 \(s\) 前 \(4\) 个中的前面 \(3\) 个一样都是 \(ABC\),所以回退到 \(3\)。
这里,我们为了预处理,简化一下,来发下一下性质。还是这个例子:
| 下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| root | A | B | C | D | \(\red{A}\) | \(\red{B}\) | \(\red{C}\) | D | A | B | C | E |
| s | \(\green{A}\) | \(\green{B}\) | \(\green{C}\) | D | \(\purple{A}\) | \(\purple{B}\) | \(\purple{C}\) | E |
我们要用到的是红色和绿色的字母,这里就是最长公共前后缀。但是,大家想一想,紫色部分和红色部分是不是一模一样的呀,因为指针能到 \(7\),肯定是因为紫色部分和红色部分一样。
那这样子,紫色和红色一样,红色和绿色一样,那绿色就和紫色一样!这样,我们就只需要预处理 \(s\) 就可以了。
3.2 具体实现
到这里,我们可以写出伪代码(\(next\) 为 \(s\) 的最长公共前后缀):
if root[l] = s[r] then
l <- l + 1, r <- r + 1
else then
r <- next[r]
那怎么预处理 \(next\) 呢?再举一个例子:
| 下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| s | a | b | a | b | \(\red{e}\) | f | a | b | a | b | _ |
next[0]=0
next[1]=0
next[2]=0
next[3]=1
next[4]=2
next[5]=0
next[6]=0
next[7]=1
next[8]=2
next[9]=3
next[10]=4
我们试着用动态规划的思想,已知 \(next_{1\sim10}\),求 \(next_{11}\)。
已知:\(s_{1\sim4}=s_{7\sim10}\)。
- 如果 \(s_5=s_{11}=e\),那么 \(next_{11}=next_{10}+1\)。
- 如果 \(s_{11}=a\),那么,取次长公共前后缀,就是 \(ab\),长度为 \(2\),就是 \(next[next[10]]\),刚好可以与 \(a\) 搭配,形成 \(aba\),则 \(next_{11}=next_{next_{10}}+1=3\)。
- 如果 \(s_{11}=x\),什么都不是,那就只能是 \(0\)了。可以转化为不断地取次长公共前后缀,但是都不符合,最后变成 \(0\) 了而已。
综上,我们得出,只要 \(s_{i+1}\ne s_{r+1}\),就让 \(r\gets next_r\),前提是 \(r\ne0\)。
最后就让 \(next_{i+1}=next_{r}+1\) 就行咯。
代码:
inline void init() {
nex[0] = nex[1] = 0; // 初始化
int m = t.size();
for (int i = 1, j = 0; i < m; i++) { // 求 nex[i+1]
while (j && s[i + 1] != s[j + 1]) j = nex[j]; // 取次长
if (s[i + 1] == s[j + 1]) j++; // 为什么不统一+1?因为上面可能是因为j=0而终止的。
nex[i + 1] = j;
}
return;
}
求出现位置与 \(next\) 数组。
代码:
// Problem: P3375 【模板】KMP
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3375
// Memory Limit: 512 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
/*+ Nimbunny +*/
#include <bits/stdc++.h>
#define endl '\n'
#define pi pair<int, int>
// #define int long long
// #pragma GCC optimize(2)
using namespace std;
const int INF = INT_MAX;
const int mod = 1e9 + 7;
const int N = 1e6 + 10;
string root, s;
int n, m, nex[N];
inline int read() {
int x;
cin >> x;
return x;
}
inline void init() {
nex[0] = nex[1] = 0; // 初始化
for (int i = 1, j = 0; i < m; i++) { // 求 nex[i+1]
while (j && s[i + 1] != s[j + 1]) j = nex[j]; // 取次长
if (s[i + 1] == s[j + 1]) j++; // 为什么不统一+1?因为上面可能是因为j=0而终止的。
nex[i + 1] = j;
}
return;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> root >> s;
n = root.size(), m = s.size();
root = "#" + root, s = "#" + s;
init();
for (int l = 0, r = 0; l < n; l++) {
while (r && root[l + 1] != s[r + 1]) r = nex[r];
if (root[l + 1] == s[r + 1]) r++;
if (r == m)
cout << l + 1 - m + 1 << endl; // 因为l还没有+1,所以l要先+1
}
for (int i = 1; i <= m; i++) cout << nex[i] << " ";
return 0;
}
3.3 时间复杂度
鸽子咯咯咯子鸽。
[学习笔记] KMP算法——烤馍片(超详细)的更多相关文章
- 学习笔记-KMP算法
按照学习计划和TimeMachine学长的推荐,学习了一下KMP算法. 昨晚晚自习下课前粗略的看了看,发现根本理解不了高端的next数组啊有木有,不过好在在今天系统的学习了之后感觉是有很大提升的了,起 ...
- [一本通学习笔记] KMP算法
KMP算法 对于串s[1..n],我们定义fail[i]表示以串s[1..i]的最长公共真前后缀. 我们首先考虑对于模式串p,如何计算出它的fail数组.定义fail[0]=-1. 根据“真前后缀”的 ...
- 【学习笔记】:JavaScript基础知识超详细总结!
目录 一.JavaScript的实现 二.JavaScript语言的特点 三.JS与HTML如何结合 四.JS中的数据类型 四.JS的原始数据类型 2.JS的引用数据类型 五.JS引用数据类型之函数 ...
- [ML学习笔记] XGBoost算法
[ML学习笔记] XGBoost算法 回归树 决策树可用于分类和回归,分类的结果是离散值(类别),回归的结果是连续值(数值),但本质都是特征(feature)到结果/标签(label)之间的映射. 这 ...
- 【Redis】命令学习笔记——键(key)(20个超全字典版)
安装完redis和redis-desktop-manager后,开始学习命令啦!本篇基于redis 4.0.11版本,从对键(key)开始挖坑! 准备工作,使用db1(默认db0,由于之前练习用db0 ...
- 学习笔记 - Manacher算法
Manacher算法 - 学习笔记 是从最近Codeforces的一场比赛了解到这个算法的~ 非常新奇,毕竟是第一次听说 \(O(n)\) 的回文串算法 我在 vjudge 上开了一个[练习],有兴趣 ...
- 算法笔记--KMP算法 && EXKMP算法
1.KMP算法 这个博客写的不错:http://www.cnblogs.com/SYCstudio/p/7194315.html 模板: next数组的求解,那个循环本质就是如果相同前后缀不能加上该位 ...
- 来去学习之---KMP算法--next计算过程
一.概述 KMP算法是一种字符串匹配算法,比如现有字符串 T:ABCDABCDABCDCABCDABCDE, P:ABCDABCDE P字符串对应的next值:[0,0,0,0,1,2,3,4,0] ...
- 学习笔记——EM算法
EM算法是一种迭代算法,用于含有隐变量(hidden variable)的概率模型参数的极大似然估计,或极大后验概率估计.EM算法的每次迭代由两步组成:E步,求期望(expectation):M步,求 ...
- 数据挖掘学习笔记--AdaBoost算法(一)
声明: 这篇笔记是自己对AdaBoost原理的一些理解,如果有错,还望指正,俯谢- 背景: AdaBoost算法,这个算法思路简单,但是论文真是各种晦涩啊-,以下是自己看了A Short Introd ...
随机推荐
- 逻辑与(&)、短路与(&&)、逻辑或(|)、短路或(||)
目录 逻辑与(&).短路与(&&).逻辑或(|).短路或(||)的区别 逻辑与(&) 短路与(&&) 逻辑或(|) 短路或(||) 逻辑与(&) ...
- 深度评测:DeepSeek API 在代码分析与审查中的实战应用 (附 Prompt 示例)
人工智能(AI)正在深刻改变软件开发的各个环节,从最初的需求分析 1 到最终的部署维护 1,AI 的身影无处不在.尤其在代码编写.分析和审查方面,AI 辅助工具如 GitHub Copilot 8 已 ...
- 解决MySQL 8.0 设置简单密码报错ERROR 1819 (HY000): Your password does not satisfy the current policy require...
MySQL8.0下设置简单密码出现错误提示:ERROR 1819 (HY000): Your password does not satisfy the current policy requirem ...
- 为什么我们痴迷于令人兴奋的 ChatGPT AI 聊天机器人
.markdown-body { color: rgba(89, 89, 89, 1); font-size: 15px; font-family: -apple-system, system-ui, ...
- RPC实战与核心原理之动态分组
动态分组:超高效实现秒级扩缩容 回顾 在 RPC 里面怎么支持流量回放,应用在引入 RPC 后,所有的请求都会被 RPC 接管,而我们在 RPC 里面引入回放的原因也很简单,就是想通过线上流量来验证改 ...
- C#之并发字典
internal class Program { const string Item = "Dictionary item"; const int Iterations = 100 ...
- C#之线程基础
创建线程 using System; using System.Threading; using System.Threading.Tasks; namespace threadDemo { clas ...
- 深入浅出了解生成模型-2:VAE模型原理以及代码实战
From: https://www.big-yellow-j.top/posts/2025/05/11/VAE.html 前文已经介绍了GAN的基本原理以及代码操作,本文主要介绍VAE其基本原理以及代 ...
- 利用DeepSeek与Python自动生成测试用例!
在当今快节奏的软件开发领域,自动化测试已然成为保障软件质量的中流砥柱.传统手动编写测试用例的方式,非但耗时费力,还极易遗漏关键场景. 所幸,AI 技术的飞速发展为我们带来了全新的解决方案.今天,就让我 ...
- Deep Learning Book在线阅读
1.Deep Learning for Anomaly Detection https://ff12.fastforwardlabs.com/