KMP&Z函数详解
KMP
一些简单的定义:
- 真前缀:不是整个字符串的前缀
- 真后缀:不是整个字符串的后缀
当然不可能这么简单的,来个重要的定义
- 前缀函数:
给定一个长度为的字符串,其 为一个长度为的数组,其中表示- 如果字串存在一对相等的真前缀和真后缀,则为这个真前缀(真后缀)的长度
- 如果有不止一对,则为其中最长的一对
- 若没有最长的,则
简而言之,为字串最长相等的真前缀和真后缀长度
特别的,规定=0
举个例子,对于字符串"",其前缀函数
考虑如何去求前缀函数,最暴力的做法肯定使的,枚举前缀位置、真前缀(真后缀)的长度和真前缀(真后缀)的每一位,代码就不放出来了(其实是没写)
优化一
我们发现当时,最多,也就是说我们枚举的长度时,上界为,这样复杂度可以被优化为
代码如下
void Getnex(string str){
int len=str.size();
nex[0]=0;
for (int i=1;i<n;i++) {
for (int j=nex[i-1];j>=0;j--) {
if (str.substr(0,j)==str.substr(i-j+1,j)) {
nex[i]=j;
break;
}
}
}
}
如何证明这个复杂度呢
我们发现每次进行一次操作,都意味着的值都会
显然有一种最坏的情况是,的值先变成,然后再掉回
容易看出这不会超过次,然后每次的复杂度为
所以总复杂度
优化二
我们在优化一中发现了一个性质,当时,
考虑把这个性质推下去,当时
我们发现,我们要找的转移点是要满足的前缀性质的
即满足
当不行时我们自然要去找下一个满足性质的转移点
很容易想到这东西不就是
由于真前缀和真后缀相等,所以必然满足既是真前缀的真前缀又是真后缀的真后缀
可能有点绕,举个例子""(下标从开始)
当我们求时,前面的值为
我们发现
那么我们就需要找到下一个转移点满足
因为所以的满足前缀性质的真前缀(真后缀)为
所以对于字符串""满足前缀性质的真前缀(真后缀)一定满足的前缀性质
所以转移点即为
至此,求前缀函数便可以优化成了
void Getnex(std::string S) {
for (int i=2,j=0;i<S.size();i++) {
while(j && S[j+1]!=S[i]) j=nex[j];
if (S[j+1]==S[i]) j++;
nex[i]=j;
}
}
前话到此完结,接下来是真正的
是对于前缀函数的典型运用
举个例子,给定一个文本和字符串,我们尝试求出在中的所有出现
我们记为和的长度
我们构造一个字符串为'',其中为不在中出现的分隔符
计算出这个字符串的前缀函数,考虑这个前缀函数除去前个值意味着什么
根据定义,为右端点为,且为一个前缀的最长真子串长度
且由于有分割符的存在,不可能超过
当时,则意味着在中完整出现一次,其右端点为
因此可以在的复杂度内解决问题
void KMP(std::string S,std::string T) {
for (int i=1,j=0;i<S.size();i++) {
while(j && T[j+1]!=S[i]) j=nex[j];
if (T[j+1]==S[i]) j++;
if (j==m-1) {
std::cout<<i-m+2<<std::endl;
j=nex[j];
}
}
}
字符串的周期
定义:
-
对于字符串,若存在满足,则为的周期
-
对于字符串,若存在满足长度为的前缀和长度为的后缀相等,则称长度为的前缀是的
由这两个定义不难看出是的周期
根据前缀函数的定义我们可以得出所有长度,即
所以我们可以在的时间复杂度内求出的所有周期
其中最小周期为
统计每个前缀的出现次数
以下默认字符串下标从开始
主要是两种问题,一个是求的前缀在中的出现次数,另一个是求的前缀在另一个字符串中的出现次数
考虑位置的前缀函数值,根据定义,其意味着字符串一个长度为 的前缀在位置出现并以为右端点,同时不存在一个更长的前缀满足前述定义。
与此同时,更短的前缀可能以该位置为右端点。
容易看出,我们遇到了在计算前缀函数时已经回答过的问题:给定一个长度为的前缀,同时其也是一个右端点位于的后缀,下一个更小的前缀长度是多少?该长度的前缀需同时也是一个右端点为的后缀。
因此以位置为右端点,有长度为的前缀,有长度为的前缀,等等,直到长度变为0。
故而我们可以通过下述方式计算答案。
void Getcnt(std::string str) {
for (int i=1;i<str.size();i++) ans[nex[i]]++;
for (int i=str.size()-1;i>0;i--) ans[nex[i]]+=ans[i];
for (int i=1;i<str.size();i++) ans[i]++;
}
例题:CF432D Prefixes and Suffixes
题意:
给你一个长度为n的长字符串,“完美子串”既是它的前缀也是它的后缀,求“完美子串”的个数且统计这些子串的在长字符串中出现的次数
模板题,直接写就行
#include <ctime>
#include <cstdio>
#include <iostream>
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
const int maxn=1e5+5;
int n,nex[maxn],ans[maxn];
std::string str;
void chkmax(int &x,int y) {if (x<y) x=y;}
void chkmin(int &x,int y) {if (x>y) x=y;}
int read() {
int x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {if (ch=='-') f=-1;ch=getchar();}
while(ch<='9' && ch>='0') {x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return x*f;
}
void Getnex(std::string s) {
for (int i=2,j=0;i<s.size();i++) {
while(j && s[j+1]!=s[i]) j=nex[j];
if (s[j+1]==s[i]) j++;
nex[i]=j;
}
}
void out(int i,int cnt) {
if (!i) {std::cout<<cnt<<std::endl;return ;}
out(nex[i],cnt+1);
std::cout<<i<<' '<<ans[i]<<std::endl;
}
int main() {
std::cin>>str;
str=" "+str;
Getnex(str);
// for (int i=1;i<str.size();i++) std::cout<<nex[i]<<' ';puts("");
for (int i=1;i<str.size();i++) ans[nex[i]]++;
for (int i=str.size();i>=1;i--) ans[nex[i]]+=ans[i];
for (int i=1;i<str.size();i++) ans[i]++;
out(str.size()-1,0);
return 0;
}
Z函数(扩展KMP)
定义:
对于一个字符串,表示和的(最长公共前缀)的长度,则被称为的函数
我们在计算的时候,可以通过前面已知的来计算
对于,我们称区间为的匹配段
我们在算法过程中维护右端点最靠右的匹配段,记作
则有为的前缀,并且在计算时我们保证
算法流程
最开始时,
在计算的过程中
-
若,则有,所以
- 若,则
- 若,我们令,然后暴力向后枚举字符
-
若,我们直接暴力从开始比较,求出
-
求出后,还要更新
void GetZ(std::string s) {
int l=0,r=0;
for (int i=1;i<s.size();i++) {
if (i<=r && z[i-l]<r-i+1) z[i]=z[i-l];
else {
z[i]=std::max(0,r-i+1);
while(i+z[i]<s.size() && s[z[i]]==s[i+z[i]]) z[i]++;
}
if (i+z[i]-1>r) {l=i;r=i+z[i]-1;}
}
}
复杂度分析
对于内层的,每次执行都会使向后移动至少一位,而,所以总共最多做次
加上外层的,总复杂度
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】