[笔记](更新中)KMP

news/2025/1/19 11:07:51/文章来源:https://www.cnblogs.com/Sinktank/p/18336754

算法详解

KMP是一种字符串匹配算法,可以在线性的时间复杂度内解决字符串的“模式定位”问题,即:
在字符串 \(A\) 中查找字符串 \(B\) 出现的所有位置。

我们称 \(A\) 为主串,\(B\) 为模式串。下文都用\(n\)表示\(A\)的长度,\(m\)表示\(B\)的长度,下标从\(1\)开始。

初始状态,我们用两个指针 \(i,j\) 分别指向 \(A\)\(B\) 的第 \(1\) 位。

KMP的主过程如下(后面会证明它的正确性):

  • 如果\(A[i]=B[j]\),则i++j++
  • 如果\(A[i]\neq B[j]\)
    • 如果\(j>1\),则j=nxt[j-1]+1
    • 如果\(j\le 1\),则i++

我们定义\(nxt[0]=0\)

其中,\(nxt\)数组是一个用于表示发生失配后,\(j\)回退到的位置。具体求法待会会说,下面我们用一个例子来理解\(nxt\)的功能,我将\(nxt\)数组画在图片的下方了。

  • 初始状态。\(i=1,j=1\)
  • \(A[i]=B[j]\),同时右移\(i,j\)\(i=2,j=2\)
  • \(A[i]=B[j]\),同时右移\(i,j\)\(i=3,j=3\)
  • \(A[i]=B[j]\),同时右移\(i,j\)\(i=4,j=4\)
  • \(A[i]=B[j]\),同时右移\(i,j\)\(i=5,j=5\)
  • \(A[i]=B[j]\),同时右移\(i,j\)\(i=6,j=6\)
  • 发现\(A[i]\neq B[j]\),于是查表\(nxt[j-1]+1=nxt[5]+1=3\),于是\(j=3\)。此时\(i=6\)
    可以发现此时灰色箭头指向的两部分是相等的。
    进一步来说,从上一张图开始,下划线的部分都是相等的。
  • OK我们继续,\(A[i]=B[j]\),于是\(i,j\)同时右移,\(i=7,j=4\)
  • 发现\(A[i]\neq B[j]\),查表得\(nxt[3]+1=2\),于是\(j=2\)。此时\(i=7\)
    我们又发现\(A\)\(B\)的下划线部分重合了,所以仍然可以从重合部分之后开始比较。
  • 然后\(A[i]=B[j]\)……就一直比到结束了,就不放图了w。

我们可以把根据上面的过程(KMP主过程)写出代码(下标从\(1\)开始):

i=1,j=1;
while(i<=n){if(a[i]==b[j]) i++,j++;else if(j>1) j=nxt[j-1]+1;else i++;if(j==m+1) cout<<i-j+1<<"\n";//输出配对的开始位置,理解一下这里
}

可以发现\(i\)是始终不降的,比起暴力,KMP通过避免\(i\)指针的回滚操作,减少了时间开销。

到这里我们已经可以初步理解\(nxt\)的含义,它是用来帮我们了解发生失配时,\(j\)应该回退到什么位置。

再进一步分析,我们发现:\(nxt[x]\),其实就是\(B[1\sim x]\)这一段的最长公共前后缀长度。

举个例子,YABAPQYABZABAAPYABAPQYABA。这是一个长度为\(24\)的字符串,那么当它作为\(B\)时:

  • \(nxt[23]=9\),如你所见,红色部分是\(B[1\sim 23]\)的公共前后缀,它的长度为\(9\)
  • \(nxt[9]=3\),因为YAB\(B[1\sim 9]\)的公共前后缀,它的长度为\(3\)
  • \(nxt[3]=0\),因为\(B[1\sim 3]\)没有公共前后缀。
  • \(nxt[24]=0\),因为\(B[1\sim 24]\)没有公共前后缀。

注意:公共前后缀可能有重叠部分,但是一个字符串不能作为它自己的公共前后缀(严格来说,我们把这种前/后缀称作前/后缀,所以后面都带“真”来称呼了)。

我们规定,\(nxt[x]\)表示的必须是\(B[1\sim x]\)这一段的最长公共真前后缀长度,这么定义是为了保证正确性,如果不规定最长,不能保证答案的正确性。后面的证明会提到。

现在我们的首要问题是,如何求\(nxt\)数组?

我们假定\(i\)之前的答案都已经求出,现在需要求\(nxt[i]\)

我们设\(j\)\(nxt[i-1]+1\),如下图:

最简单的情况自然就是\(B[i]=B[j]\),那么\(nxt[i]=j\),这是显然的。

但是也可能遇到更棘手的情况:

\(B[i]\neq B[j]\),怎么办呢?

既然YABAPQYAB这个公共真前后缀不行,那就再找一个更小的!

显然更小的前后缀就是YAB了,怎么得来的呢?就是看\(nxt[j-1]\)的值是多少,那么新的\(j\)就是\(nxt[j-1]+1\)。此时再看看\(B[i]=B[j]\)是否成立。成立的话就\(nxt[i]=j\),不成立就继续找\(j\),直到找到\(B[i]=B[j]\),或者\(j=1\)为止。

可以发现我们充分利用了之前计算出的\(nxt\),将原规模逐步缩小,是不是有点归纳法的意味?

再来举一个不断更新\(j\)直到\(j=1\)为止的例子:

我们可以把这一步骤也写成代码(下标从\(1\)开始):

int i=2,j=1;//nxt[1]应为0,所以i从2开始
while(i<=m){if(b[i]==b[j]) nxt[i++]=j++;else if(j>1) j=nxt[j-1]+1;else nxt[i++]=0;
}

(是不是和上面的代码很像)


至此把两个代码拼接起来就能得到KMP的全过程了。

P3375 【模板】KMP

下标从$1$开始
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
string a,b;
int n,m,nxt[N];
int main(){cin>>a>>b;n=a.size(),m=b.size();a=' '+a,b=' '+b;int i=2,j=1;while(i<=m){if(b[i]==b[j]) nxt[i++]=j++;else if(j>1) j=nxt[j-1]+1;else nxt[i++]=0;}i=1,j=1;while(i<=n){if(a[i]==b[j]) i++,j++;else if(j>1) j=nxt[j-1]+1;else i++;if(j==m+1) cout<<i-j+1<<"\n";}for(int i=1;i<=m;i++) cout<<nxt[i]<<" ";return 0;
}
下标从$0$开始
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
string a,b;
int n,m,nxt[N];
int main(){cin>>a>>b;n=a.size(),m=b.size();int i=1,j=0;while(i<m){if(b[j]==b[i]) nxt[i++]=++j;else if(j) j=nxt[j-1];else nxt[i++]=0;}i=0,j=0;while(i<n){if(a[i]==b[j]) i++,j++;else if(j) j=nxt[j-1];else i++;if(j==m) cout<<i-j+1<<"\n";}for(int i=0;i<m;i++) cout<<nxt[i]<<" ";return 0;
}

正确性证明

我们刚接触\(nxt\)的定义时,可能会有疑问:为什么\(nxt\)一定要表示\(B[1\sim x]\)这一段的最长公共真前后缀长度?

我们用反证法证明一下正确性,顺带解决这个问题。


根据KMP的流程,如果遇到失配的情况,我们先找\(B[1\sim (j-1)]\)中的最长公共真前后缀(如图,用\(f\)表示)的长度\(nxt[j-1]\),然后把\(j\)置为\(nxt[j-1]+1\),如下图。

要证明KMP是正确的,就是证明\(j\)回退时不会跳过正确答案。
我们假设在\(j>nxt[j-1]+1\)时存在匹配,如下图,那么灰色箭头连接的部分都相等。
自然,黄色大括号部分(不包含\(i,j\))也相等,那么黄色部分的长度\(=j-1\)

回到第\(1\)张图(\(j'\)表示第\(1\)张图情况下的\(j\)),根据假设,\(j>|f|+1\),即黄色部分长度\(>|f|\)
这样的话,黄色部分就成了\(B[1\sim j']\)最长的公共真前后缀,与“\(f\)\(B[1\sim j']\)最长公共真前后缀”矛盾。

得到的结论是:\(j>nxt[j-1]+1\)时一定不存在匹配。

也就是说\(j\)回退是不会漏掉解的,正确性得证。

这样我们的问题也迎刃而解了:如果\(nxt\)不是最长,上面的情况就可能不形成矛盾,进而\(j>nxt[j-1]+1\)时也可能存在解,而这被我们忽略掉了,所以正确性不能保证。

复杂度证明

通过看代码能发现两部分代码相似,所以就一块说了:

\(i\)每增加\(1\)\(j\)最多也增加\(1\),从而\(j\)最多增加\(len\)次,进而最多减少\(len\)次。
所以处理\(nxt\)\(O(m)\)的,主过程是\(O(n)\)的,总时间复杂度就是\(O(n+m)\)了。

——字符串学习笔记 · 浅析KMP——单模式串匹配算法 - 皎月半洒花

\(\mathcal{NEXT\ \ \ PHANTASM...}\)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/776408.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

抖音多账号授权给独立客服系统,实现评论私信互动与自动化回复

在当今数字化时代,社交媒体平台如抖音已成为企业与客户互动的重要渠道。为了更好地管理客户关系并提升服务质量,企业需要有效地整合抖音账号与客服系统。本文将详细介绍两种抖音授权接入方式,以及如何利用这些授权提升客户互动和自动化回复的效率。 演示网站:gofly.v1kf.co…

【Mysql】Docker下Mysql8数据备份与恢复

目录【Mysql】Docker下Mysql8数据备份与恢复1 创建Mysql容器2 连接Mysql3 Binlog检查3.1 检查是否开启3.2 mysql 5.73.3 mysql84 备份数据库4.1 容器里执行备份4.2 宿主机执行备份4.3 参数说明5 定时备份!/bin/bash按 shift + :输入 wq 【Mysql】Docker下Mysql8数据备份与恢复 …

【Windows】安装WSL

1、启用子系统2、商店搜wsl并下载3、重启电脑生效

前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线

本章响应小伙伴的反馈,除了算法自动画连接线(仍需优化完善),实现了可以手动绘制直线、折线连接线功能。本章响应小伙伴的反馈,除了算法自动画连接线(仍需优化完善),实现了可以手动绘制直线、折线连接线功能。请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 B…

2024牛客多校第5场

很神奇的场hh,大家一起坐牢,多好啊! B 找规律,这种题一般都是多模拟几个数据然后猜出来#include<bits/stdc++.h> using namespace std;inline int read() {int x=0;bool f=1;char ch=getchar();for(;ch<0||ch>9;ch=getchar())f^=(ch==-);for(;ch>=0&&am…

15. 序列化模块json和pickle、os模块

1. 序列化模块1.1 序列化与反序列化 (1)序列化 将原本的python数据类型字典、列表、元组 转换成json格式字符串的过程就叫序列化 (2)反序列化 将json格式字符串转换成python数据类型字典、列表、元组的过程就叫反序列化 (3)为什么要序列化 计算机文件中没有字典这种数据类…

AI表情神同步!LivePortrait安装配置,一键包,使用教程

快手在AI视频这领域还真有点东西,视频生成工具“可灵”让大家玩得不亦乐乎。现在又开源了一款超好玩的表情同步(表情控制)项目。 一看这图片,就知道是小视频平台出的,充满了娱乐性。发布没几天就已经有8000+Star。项目简介 LivePortrait 是一款由快手团队开发的高效肖像动…

右下角wifi图案点击无可用wifi/更新网卡驱动时遇到错误代码56的解决办法

1.问题 如下图所示,我这里遇到明明有wifi,但是无法检索到任何有用wifi的情况。2.解决方法 参考:电脑WIFI消失,网卡驱动Intel(R) Wi-Fi 6 AX201 160MHz感叹号报错 解决方案集合 —— 无线WI-FI功能缺失,Intel(R) Wi-Fi 6 AX201 160MHz异常,驱动更新错误 2.1 问题原因 当时…

单例模式及其思想

本文包括以下几点↓结论:设计模式不是简单地将一个固定的代码框架套用到项目中,而是一种严谨的编程思想,旨在提供解决特定问题的经验和指导。 单例模式(Singleton Pattern) 意图 旨在确保类只有一个实例,并提供一个全局访问点以访问该实例。 适用性 当你希望系统中只有一…

数组part02

2024年8月1日,今天学习了数组的第二部分。 1.巩固了昨天的双指针问题,即滑动窗口/双指针;注意,双指针是为了减少for循环,使用的时候小心循环的写法和快慢指针的增长方法。 2.学习了数组模拟的螺旋矩阵问题,注意循环不变量; 3.学习了前缀和的方法,前缀和常用来解决区间和…

windwos文件句柄数限制

1、修改注册表,位置如下: HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/Windows2、设置 1、GDIProcessHandleQuota此项设置GDI句柄数量,默认值为2710(16进制)/10000(10进制),该值的允许范围为 256 ~ 16384 ,将其调整为大于默认的10000的值。如果您的…

P5665 [CSP-S2019] 划分

讲解 P5665 [CSP-S2019] 划分。由朴素 dp 入手,先用二分优化,然后用走指针优化,之后注意到单调性,将状态数压缩,然后使用单调队列优化转移。思路: 首先求出 \(a\) 的前缀和数组 \(s\)。 考虑动态规划,令 \(dp_{i,j}\) 表示以 \(i\) 结尾,末尾有 \(j\) 个为一组的最小答…