【算法】KMP算法
upd: 2021.4.13
目录
简介
前置知识
原理
例题
简介
KMP 算法由 三位科学家提出,可用于在一个 文本串 中寻找某 模式串 存在的位置。
本算法可以有效降低在一个 文本串 中寻找某 模式串 过程的时间复杂度。(如果采取朴素的想法则复杂度是 )
题面:https://www.luogu.com.cn/problem/P3375
这里朴素的想法指的是枚举 文本串 的起点,然后让 模式串 从第一位开始一个个地检查是否配对,如果不配对则继续枚举起点。
前置知识
真前缀
指字符串左部的任意子串(不包含自身),如 abcde
中的 a
,ab
,abc
,abcd
都是真前缀但 abcde
不是。
真后缀
指字符串右部的任意子串(不包含自身),如 abcde
中的 e
,de
,cde
,bcde
都是真后缀但 abcde
不是。
前缀函数
一个字符串中最长的、相等的真前缀与真后缀的长度, 如AABBAAA
对应的前缀函数值是 。
原理
注意:在分析的时候,我们规定字符串的下标从 开始。
开始:
我们记扫描模式串的指针为j
,而扫描文本串的指针为i
,假设一开始i,j
都在起点,然后让它们一直下去直到完全匹配或者失配,比如:
j
ABCD
i
ABCDEFG
然后
j
ABCD
i
ABCDEFG
最后在此完成了一次匹配,类似地如果ABCD
改为ABCC
则在此失配。
j
ABCD
i
ABCDEFG
i,j运作模式如上。
KMP算法就是,当模式串和文本串失配的时候,j
指针从真后缀的末尾跳到真前缀的末尾,然后从真前缀后一位开始继续匹配。(从而起到减少配对次数,这便是KMP算法的核心原理)
一个比较感性的理解是:尽可能地减少必然失败的匹配。
结合例子解释:
模式串:
文本串:
j
指针在最后一个A处失配。
j
AABBAAA
i
AABBAABBAAA
因为此时 以 为尾的前缀 所对应的前缀函数值是 ,所以 j指针
跳到这里:
j
AABBAAA
i
AABBAABBAAA
然后从下一位开始继续配对:
j
AABBAAA
i
AABBAABBAAA
最后
j
AABBAAA
i
AABBAABBAAA
可以看出,KMP 能够有效减少配对次数。
实现
我们记
模式串
为p
,文本串
为s
。
从上面的模拟中,我们发现需要预处理出一个数组(记之为next[]
),它储存模式串中前缀对应的前缀函数,如对于字符串ABCABC
:
(因为什么都没有)
(A
甚至没有真前缀和真后缀)
(AB
)
(ABC
)
(ABCA
)
(ABCAB
)
(ABCABC
)
同样地,我们发现如果用暴力朴素的想法来统计复杂度是 不好,于是采用类似于上面的方法,只不过模式串配对的对象是自己罢了。
可以结合代码理解,并注意举例,尝试在纸上模拟这个过程。
for(int i=2,j=0;i<=lenp;i++){
while(j && p[j+1]!=p[i]) j=nxt[j]; // 如果j指向元素的下一个元素会和当前配对位置失配,则j跳回去
if(p[j+1]==p[i]) j++; //如果能够配对上,j++
nxt[i]=j; //记录当前位置的前缀函数π
}
完整代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
char p[N],s[N];
int nxt[N];
int main(){
cin>>s+1>>p+1;
int lenp=strlen(p+1),lens=strlen(s+1);
// build next array
for(int i=2,j=0;i<=lenp;i++){
while(j && p[j+1]!=p[i]) j=nxt[j]; // 如果j指向元素的下一个元素会和当前配对位置失配,则j跳回去
if(p[j+1]==p[i]) j++; //如果能够配对上,j++
nxt[i]=j; //记录当前位置的前缀函数π
}
for(int i=1,j=0;i<=lens;i++){
while(j && p[j+1]!=s[i]) j=nxt[j];
if(p[j+1]==s[i]) j++;
// if match
if(j==lenp){
j=nxt[j];
cout<<i-lenp+1<<endl;
}
}
for(int i=1;i<=lenp;i++) cout<<nxt[i]<<' ';
cout<<endl;
return 0;
}
复杂度
例题
https://www.acwing.com/problem/content/1054/
分析
结合了状态机,KMP的一道DP题。
递推方程:f[i][u]+=f[i-1][j]
其中j
表示一个状态,u
表示由j
转移而来的状态,i
表示填到文本串的第几位了。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=55, mod=1e9+7;
int nxt[N],f[N][N];
int n;
char p[N];
int main(){
cin>>n>>p+1;
int lenp=strlen(p+1);
for(int j=0,i=2;i<=lenp;i++){
while(j && p[j+1]!=p[i]) j=nxt[j];
if(p[j+1]==p[i]) j++;
nxt[i]=j;
}
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<lenp;j++)
for(char k='a';k<='z';k++){
int u=j;
while(u && p[u+1]!=k) u=nxt[u];
if(p[u+1]==k) u++;
if(u<lenp) f[i][u]=(f[i-1][j]+f[i][u])%mod;
}
int res=0;
for(int i=0;i<lenp;i++) res=(f[n][i]+res)%mod;
cout<<res<<endl;
return 0;
}
https://www.luogu.com.cn/problem/CF1200E
分析
思路很简单的一道题,我们记已经拼接好的串为res
,然后新输入的串为rec
,只需建立一个tmp
将它们以rec
在前,res
在后的顺序拼成一个串,用 KMP 算出这个串的前缀函数,从而得到rec
需要将头部删去多少个数再拼上res
即可,但直接这样做会导致后面res
的长度非常大,从而导致 KMP 的时候TLE,所以要取rec
,res
较短的长度切片再拼接,细节见代码。
代码
#pragma GCC optimize("O3")
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int nxt[N];
string res,rec; // 答案串、记录输入的串
int main(){
ios::sync_with_stdio(false);
int n;
cin>>n;
n--; // 先读入res中
cin>>res;
while(n--){
cin>>rec;
string tmp;
int len=min(rec.size(),res.size());
tmp=' '+rec.substr(0,len)+"!";
//将串的下标定为 1 ,同时加一个其他符号防止前缀函数值比min(rec,res)还大
tmp+=res.substr(res.size()-len,len);
//kmp
nxt[0]=nxt[1]=0;
for(int i=2,j=0;i<tmp.size();i++){
while(j && tmp[j+1]!=tmp[i]) j=nxt[j];
if(tmp[j+1]==tmp[i]) j++;
nxt[i]=j;
}
int x=nxt[tmp.size()-1];
rec.erase(0,x);
res+=rec;
//因为这里我卡了很久,如果写成res=res+rec 会导致TLE 因为这样写的复杂度是O(N^2)而res+=rec是线性复杂度
}
cout<<res<<endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探