浅析 KMP
由于考场上被 KMP 创飞了,怕下次考 KMP 所以重拾 KMP。
前言
KMP 是由 D.E.Knuth,J.H.Morris 和 V.R.Pratt 于 1977 年发明,KMP 以三人名字首字母命名。
原文地址:KMP_1977。
中文翻译(由 deepseek 完成):KMP 中文。
说实话我最开始学 KMP 的时候没听懂除了我摸鱼以外就是看到 oi-wiki 上的解释太长了我懒得看。
所以为了帮助那些懒得看的同学我写了这篇文章。
其实我写完后发现还是很长。
正文
我们根据 oi-wiki 上对前缀函数进行定义:
给定一个长度为
- 如果子串
s_{0\dots i} 有一对相等的真前缀与真后缀:s_{0\dots k-1} 和s_{i - (k - 1) \dots i} ,那么p_{i} 就是这个相等的真前缀(或者真后缀,因为它们相等)的长度,也就是p_{i}=k ; - 如果不止有一对相等的,那么
p_{i} 就是其中最长的那一对的长度; - 如果没有相等的,那么
p_{i}=0 。
然后我们来看看模板题,我们先定义
我们定义字符串
现在你已经知道了怎么
众所周知我们暴力求一个字符串
for(int i=1;i<=n;i++)
{
for(int j=i;j;j--)
{
if(s.substr(0,j)==s.substr(i-j+1,i))
{
p[i]=j;
break;
}
}
}
但是我们仔细观察一下代码发现,
但是
但是我学到这里想到为什么 while(j>0&&a[i]!=a[j+1]) j=nxt[j]; 其中 j=nxt[j] 为什么
S: [前缀]...[后缀]?
~~~~ ~~~~ ^
j个 j个 i
现在如果不匹配,我要找一个
:::success[Code]
#include<bits/stdc++.h>
using namespace std;
#ifdef __linux__
#define gc getchar_unlocked
#define pc putchar_unlocked
#else
#define gc _getchar_nolock
#define pc _putchar_nolock
#endif
#define int long long
#define ull unsigned long long
#define R register
#define rint register int
#define _ read<int>()
inline bool blank(const char x)
{
return !(x^9)||!(x^13)||!(x^10)||!(x^32);
}
template<class T>inline T read()
{
T r=0,f=1;R char c=gc();
while(!isdigit(c))
{
if(c=='-') f=-1;
c=gc();
}
while(isdigit(c)) r=(r<<1)+(r<<3)+(c^48),c=gc();
return f*r;
}
inline void out(int x)
{
if(x<0) pc('-'),x=-x;
if(x<10) pc(x+'0');
else out(x/10),pc(x%10+'0');
}
inline void read(char &x)
{
for(x=gc();blank(x)&&(x^-1);x=gc());
}
const int N=1e6+10;
int nxt[N],f[N];
signed main()
{
string a,b;
cin>>b>>a;
rint n=b.size(),m=a.size();
a=' '+a,b=' '+b;
nxt[1]=0;
for(rint i=2,j=0;i<=m;i++)
{
while(j>0&&a[i]!=a[j+1]) j=nxt[j];
if(a[i]==a[j+1]) j++;
nxt[i]=j;
}
for(rint i=1,j=0;i<=n;i++)
{
while(j>0&&(j==n||b[i]!=a[j+1])) j=nxt[j];
if(b[i]==a[j+1]) j++;
f[i]=j;
if(f[i]==m)
{
out(i-m+1);
pc('\n');
}
}
for(rint i=1;i<=m;i++)
{
out(nxt[i]);
pc(' ');
}
return 0;
}
:::
我们来分析下时间复杂度:
观察代码中的 while 循环:虽然看起来可能很慢,但实际上
-
-
-
因此总的操作次数是
\mathcal{O}(n) 的。
拓展
最小循环元
一般来说,KMP 还可以解决最小循环元的问题,我们先说结论,对于长度为