「KMP」学习笔记
前言—— 与
-
有的时候
数组确实比 好用,且字符串长度很大时 会被卡掉,所以不要犯懒,老实用 , 可以用但是慎用。 -
同时很多情况下为了方便和减少出错,我们会想办法把字符串的坐标从
变成 ,对于 和 都有办法,但不尽相同。cin>>s+1; int len=strlen(s+1);
或cin>>s; s=" "+s; int len=s.size()-1; cin>>s; int len=s.size(); s=" "+s;
定义与基本求法
-
定义:
用于匹配两字符串时的大幅度优化、
问题、模式串在主串出现的次数以及位置等一系列问题,应用广泛,下面会依次解释。-
字符串 的长度。 区间 子串的长度。 -
长度为 的前缀。 长度为 的后缀。 -
:(经常应用 的性质 )若
,则称 为 。 中 均为其 。其中前后缀追均为严格意义上,长度小于总串长度的前后缀。 -
数组:(重中之重)-
又名前缀表,
表示 的最长 长度。(基本定义) -
表示两字符进行匹配,到该元素匹配失败时,重新匹配调到的位置,避免从 开始重新匹配。故此 作为 的备选存在。 -
一定是 的 ;由此, 一定是 的 ( 表示 的长度 )。以上均可以根据其基本定义和
的性质得出。
-
-
-
基本求法:
-
和自己匹配——求
解决模式串匹配主串问题时,需要先处理出模式窜的
数组。顾名思义,就是和自己匹配.
先定义一个
,先用 区匹配 。 从 开始, 从 开始。因为 显然 。若当前匹配失败且
,根据 的基本定义,作为 的备选,另 不断跳 ,直到 ,那么此时匹配成功, 。如果一直跳到 还不能满足,便是匹配不上了,当前 。明确一个问题,在不断跳
的过程中,跳到 时,此时得到的这个 必定是 的 ,现在又满足 ,那么 就成了 的 ,且一定是最长的 ,即 。通过上述方式从前往后枚举
,枚举到 时, 原先值保留,此时 ,从而方便继续向前跳和接下来的步骤,这里需详细理解一下上一段文字。打个比方,如
: ,显然 。 , 。 , ,不断往前跳 ,始终不存在 ,故 。 ,现在经历过上一步的跳 使 , ,故 。 , 。 , ,不断向前跳 ,和第三次操作一样,始终不满足 ,故 。
也就得到了该串的
数组,即前缀表,同时表示 的最长 长度 :-
代码如下:
void kmp() { int j=0,l=strlen(s+1); for(int i=2;i<=l;i++) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[i]==s[j+1]) j++; nxt[i]=j; } }
-
和主串匹配
在此带入一道例题的情景,当然
的作用还有好多,下面的例题中还会有一定涉及。主串 ,模式串 。现已经将模式串的
处理出来,那么匹配主串就是轻而易举的了。先来看一下暴力是怎么匹配的:
可以看的出,每次匹配失败后,就从头开始重新匹配。
但使用
遍不用这样。依旧是上述的
,当匹配 和 时,如果匹配失败, 遍不断往前跳 直至可以匹配,思路和打法几乎和求 是完全一样的。如上面的例子,采用
就可以:而不必从头开始。
那么这道题要求出现的次数,那么每次
匹配到 时,也就表示模式串匹配完一遍了,记录答案 ,另 继续匹配即可。( 表示模式串的长度 )。-
代码如下:
int ask(string s,string t) { int j=0,n=s.size()-1,m=t.size()-1,ans=0; for(int i=1;i<=n;i++) { while(j&&t[j+1]!=s[i]) j=nxt[j]; if(s[i]==t[j+1]) j++; if(j==m) ans++,j=nxt[j]; } return ans; }
-
-
子串周期循环问题。
该问题下面的例题中会有详细描述,需要注重理解好
和 的含义。
-
-
关于复杂度
玄学玩意,虽然有个
但最多执行 次,最后还是看一下课件吧:
例题
-
题面:
对于一个串
,存在一个子串(长度小于主串)周期,例如 均为 的周期,其中 为最长周期,而 没有周期,则最长周期长度为 。给定一个字符串 ,求其所有前缀的最大周期长度之和。 -
解法:
先来看一张图:
也就完美的解释了这道题,这样的话就不断跳
,使得到 的最小的一个 设其为 , 即可,当然如果他的 最大就是 了, 。 -
代码如下:
#include<bits/stdc++.h> #define int unsigned long long #define endl '\n' using namespace std; const int N=1e6+10,P=1e9+7; template<typename Tp> inline void read(Tp&x) { x=0;register bool z=1; register char c=getchar(); for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0; for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48); x=(z?x:~x+1); } int n,ans,nxt[N]; char s[N]; void kmp() { int j=0,l=strlen(s+1); for(int i=2;i<=l;i++) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[i]==s[j+1]) j++; nxt[i]=j; } } signed main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); freopen("out.txt","w",stdout); #endif read(n); cin>>(s+1); kmp(); for(int i=2;i<=n;i++) { int j=i; while(nxt[j]) j=nxt[j]; if(nxt[i]) nxt[i]=j; ans+=i-j; } cout<<ans; } -
扩展:如果求最小周期呢?
根据上面的题不难相出,改成最大的
就可以了,其实就是直接的 。然后 即可,似乎更简单一点,但我们仍应该证明一下。其实也就是这道题:Radio Transmission
-
代码如下:
#include<bits/stdc++.h> #define int long long #define endl '\n' using namespace std; const int N=1e6+10,P=1e9+7; template<typename Tp> inline void read(Tp&x) { x=0;register bool z=1; register char c=getchar(); for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0; for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48); x=(z?x:~x+1); } int n,nxt[N]; string s; void kmp(string s) { int j=0; for(int i=2;i<=n;i++) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[i]==s[j+1]) j++; nxt[i]=j; } } signed main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); freopen("out.txt","w",stdout); #endif read(n); cin>>s; s=" "+s; kmp(s); cout<<n-nxt[n]; }
-
动物园
-
题面:
给定一字符串
,求其每一个前缀的长度 的 的个数。( 指该前缀的长度 ) -
解法:
在此处换一种想法,不一定非要求自身的个数,对于一个
,我们求其后面可能出现的 的 ,此处 可以通过跳 跳到 的位置,且 为其跳 过程中第一个 的位置。可能听起来不太好理解,就比方说,我现在是
,那么我的后面将有一个 需要我,那么我将要给 贡献多少的 。不同于题面,重新定义
表示 将为 贡献的值,继续上面的情景,既然我是他跳 跳过来的,那么我一定能和他的后缀构成 ,那么到我这里,他将继续向前跳一直到 ,那么此时他往前继续跳的 也一定是我的 ,既然到我这里已经 了,那么我前面的一定也满足,我不妨将我前面 的数量算上我自己一起给他,这样他就不用费劲的向前跳了。(就不会 了)看到这里好像发现了,就是对于每一个长度为
的前缀,他不断跳 ,当他跳到 时,再往前跳多少步跳到 ,就是他的 值,把这些 加起来就是最后要求的值。那么思考上面的情景,每一个
他的 就是他不断往前跳 跳多少次到 。又发现 ,于是可以线性求,在处理 数组时可以顺便求出来。 -
代码如下:
#include<bits/stdc++.h> #define int unsigned long long #define endl '\n' using namespace std; const int N=1e6+10,P=1e9+7; template<typename Tp> inline void read(Tp&x) { x=0;register bool z=1; register char c=getchar(); for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0; for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48); x=(z?x:~x+1); } int n,nxt[N],num[N]; char s[N]; void kmp() { int j=0,l=strlen(s+1); num[1]=1; for(int i=2;i<=l;i++) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[j+1]==s[i]) j++; nxt[i]=j; num[i]=num[j]+1; } } int ask() { int j=0,l=strlen(s+1),ans=1; for(int i=2;i<=l;i++) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[j+1]==s[i]) j++; while(j>(i/2)) j=nxt[j]; ans=ans*(num[j]+1)%P; } return ans; } signed main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); freopen("out.txt","w",stdout); #endif read(n); while(n--) { memset(nxt,0,sizeof(nxt)); cin>>s+1; kmp(); cout<<ask()<<endl; } }
剪花布条
-
题面:
和模式串与主串的匹配十分类似,不同的是每个匹配不可重叠:
直接匹配 应是 个,但此处顾名思义 “剪”,所以只能剪出来 个。 -
解法:
与基本求法中的匹配十分相似,只需要在匹配完一遍后不让
,而是让 即可。 -
代码如下:
#include<bits/stdc++.h> #define int long long #define endl '\n' using namespace std; const int N=1e6+10,P=1e9+7; template<typename Tp> inline void read(Tp&x) { x=0;register bool z=1; register char c=getchar(); for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0; for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48); x=(z?x:~x+1); } void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');} void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);} string s,t; int n,m,nxt[N],ans,j; signed main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); freopen("out.txt","w",stdout); #endif while(1) { //memset(nxt,0,sizeof(nxt)); cin>>s; n=s.size(); if(s=="#"&&n==1) return 0; cin>>t; m=t.size(); s=" "+s,t=" "+t; j=0; for(int i=2;i<=m;i++) { while(j&&t[j+1]!=t[i]) j=nxt[j]; if(t[i]==t[j+1]) j++; nxt[i]=j; } j=0,ans=0; for(int i=1;i<=n;i++) { while(j&&t[j+1]!=s[i]) j=nxt[j]; if(t[j+1]==s[i]) j++; if(j==m) ans++,j=0; } write(ans); puts(""); } } -
教训:
关于此题有一个深痛教训,对于
数组,即使多测,每一次也都会重新处理每个 的值,不必清空,而由于我多次 导致常数过大多次超时。所以:
题目中,不必对 数组 。
总结
当时课件讲
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效