扩展KMP学习笔记

扩展KMP学习笔记

一、用途

对于字符串\(S\),定义\(z[i]\)为从位置\(i\)开始的\(s\)后缀与\(s\)的最长公共前缀的长度。扩展\(KMP\)就是用于在\(\mathcal O(n)\)复杂度下求解\(z\)函数的算法。

二、实现

既然是扩展\(KMP\),那么当然与\(KMP\)有一定的关系:这一点主要体现在思想上,两者都在求解的过程中通过之前已经求解的答案来快速计算新的答案(实际上不少字符串算法都利用了这个思想)

假设我们已经求好了\(z[1]-z[i-1]\),现在我们要求解\(z[i]\)

根据\(z\)函数的性质,我们知道对于任意的\(i\)都有\(s[i,i+z[i]-1]=s[1,z[i]]\),这一点非常有用,于是我们维护最长的形如\([i,i+z[i]-1]\)的匹配段,记为\([l,r]\),特别注意的是\(z[1]\)显然等于\(|s|\),但它比较特殊,不算作匹配段,于是我们从\(2\)开始枚举\(z\),初始令\(l=r=0\)

计算\(z[i]\)时,如果\(i\le r\),那么根据\([l,r]\)的定义就会有\(s[i,r]=s[i-l+1,r-l+1]\),又因为根据\(z[i-l+1]\)的性质

\[s[1,z[i-l+1]]=s[i-l+1,i-l+1+z[i-l+1]-1] \]

于是有

\[s[i-l+1,r-l+1]=s[1,min(z[i-l+1],r-i+1)] \]

因此:\(z[i]\ge min(z[i-l+1],r-i+1)\),我们直接将它作为\(z[i]\)的初始值

然后我们再暴力的增加\(z[i]\)并判断是否合法,最后用\(z[i]\)来更新\(r\),这就是求解\(z\)函数的过程了。

代码如下:

inline void getz(char *s,int n){
	z[1]=n;
	for(int i=2,l=0,r=0;i<=n;++i){
		if(i<=r) z[i]=min(r-i+1,z[i-l+1]);
		while(s[i+z[i]]==s[z[i]+1]&&i+z[i]<=n) ++z[i];	
		if(i+z[i]-1>r) l=i,r=i+z[i]-1;
	}
} 

这个实现过程简单易懂,但它的复杂度为什么正确呢?

三、复杂度证明

实际上这里的复杂度与Manacher算法类似,整个过程中唯一看起来非线性的就是暴力扩展\(z[i]\)了,考虑什么时候我们会暴力扩展\(z[i]\)

  • \(i>r\)时,那么此时扩展\(z[i]\)\(i+z[i]-1\)一定\(>r\),每次扩展都会导致\(r\)增加
  • \(i<=r\)\(z[i-l+1]>r-i+1\)时,也就是说初始\(i+z[i]-1=i+r-i+1=r+1\),那么同样的每次扩展也会导致\(r\)的增加。
  • 因为\(r\)最多从\(1\)增加到\(|s|\),因此扩展不超过\(|s|\)次,总复杂度是\(\mathcal O(|s|)\)的。

四、应用

洛谷模板题

这道题目除了求解\(z\)函数之外,还要我们求解\(s\)的每一个后缀与\(t\)\(LCP\),也就是\(P\)数组,这个过程是类似的。

同样考虑我们已经求得了\(p[1-i-1]\)求解\(p[i]\),我们同样维护最长的匹配段\([i,i+p[i]-1]\),记为\([l,r]\),有\(s[l,r]=t[1,r-l+1]\)

那么同样的,当\(i\le r\)时,有\(s[i,r]=t[i-l+1,r-l+1]=t[1,min(z[i-l+1],r-i+1)]\),故我们将\(p[i]\)的初始值设为\(min(z[i-l+1],r-i+1)\),然后暴力扩展即可。它的复杂度显然也是线性的:

#include<bits/stdc++.h>
using namespace std;
const int N=2e7+10;
typedef long long ll;
char s[N],t[N];
int n,m,p[N],z[N];
inline void getz(char *s,int n){
	z[1]=m;
	for(int i=2,l=0,r=0;i<=n;++i){
		if(i<=r) z[i]=min(r-i+1,z[i-l+1]);
		while(s[i+z[i]]==s[z[i]+1]&&i+z[i]<=n) ++z[i];	
		if(i+z[i]-1>r) l=i,r=i+z[i]-1;
	}
} 
inline void exkmp(){
	getz(t,m);
	for(int i=1,l=0,r=0;i<=n;++i){
		if(i<=r) p[i]=min(r-i+1,z[i-l+1]);
		while(s[i+p[i]]==t[p[i]+1]&&i+p[i]<=n&&p[i]+1<=m) ++p[i];
		if(i+p[i]-1>r) l=i,r=i+p[i]-1;
	}
}
int main(){
//	freopen("in.in","r",stdin); 
	scanf("%s%s",s+1,t+1);
	n=strlen(s+1);m=strlen(t+1);
	exkmp();
	ll ans1=0,ans2=0;
	for(int i=1;i<=m;++i) ans1=ans1^(1ll*i*(z[i]+1));
	for(int i=1;i<=n;++i) ans2=ans2^(1ll*i*(p[i]+1));
	printf("%lld\n%lld",ans1,ans2);
	return 0;
} 

CF126B Password

题目要求我们对于字符串\(S\)求出既是\(S\)的前缀又是\(S\)的后缀同时又在\(S\)中间出现过的最长子串。

依然求出\(z\)函数,从左到右枚举,当枚举到\(i\)时,如果\(z[i]=n-i+1\),那么就以为着从\(i\)开始的后缀同时也是\(s\)的一个前缀,判断它在\(s\)中间出现过也很容易,只需要\(Max_{j=2}^{i-1}z[i]\ge n-i+1\)即可

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
char s[N];
int z[N],n;
inline void getz(char* s,int m){
	z[1]=m;
	for(int i=2,l=0,r=0;i<=m;++i){
		if(i<=r) z[i]=min(z[i-l+1],r-i+1);
		while(s[i+z[i]]==s[z[i]+1]&&i+z[i]<=m) ++z[i];
		if(i+z[i]-1>r) l=i,r=i+z[i]-1;
	}
}
int main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	getz(s,n);
	int mx=-1;
	for(int i=2;i<=n;++i){
		if(z[i]==n-i+1&&mx>=z[i]){
			for(int j=i;j<=n;++j) putchar(s[j]);
			return 0;
		}
		mx=max(mx,z[i]);
	}
	puts("Just a legend");
	return 0;
}

总结:扩展\(KMP\)算法的应用不太多,核心还是在于它这种利用之前求出的数据加速求解的思想。

posted @ 2021-01-24 13:05  cjTQX  阅读(66)  评论(0)    收藏  举报