Z-Algorithm详解

Z-Algorithm详解

0.前言

给你一个文本串ttt和一个模式串ppp,让你寻找ppp在ttt中出现的所以位置。

例如,t="abacababac"t="abacababac"t="abacababac",p="aba"p="aba"p="aba",那么ppp在ttt中出现了333次,起始位置在ttt中的下标分别是111,555,777。

很显然可以想到O(tp)O(|t|*|p|)O(∣t∣∗∣p∣)的暴力算法,即以每一个位置为起始位置,暴力匹配每一个字符。但是如果ttt和ppp的长度都是10510^5105级别的就会超时,我们需要更高效的方法。在中国有一个KMPKMPKMP算法比较流行,但是我个人比较喜欢ZalgorithmZ-algorithmZ−algorithm。这里我给大家讲一下这个。

1.一些函数的定义

我们定义zi(s)z_i(s)zi​(s)为对于所有的2is2 \leq i \leq |s|2≤i≤∣s∣,以iii开头的子串和sss的最长公共前缀的长度

如:

s="aba"s="aba"s="aba",那么z3(s)=1z_3(s)=1z3​(s)=1(以333为起始位置,能够匹配sss长度为111的前缀"a""a""a",但匹配不了长度为222的前缀"ab""ab""ab")。

s="abcabcab"s="abcabcab"s="abcabcab",那么z4(s)=5z_4(s)=5z4​(s)=5

s="abacababaca"s="abacababaca"s="abacababaca",那么z5(s)=3,z7(s)=5z_5(s)=3, z_7(s)=5z5​(s)=3,z7​(s)=5

2.如果已知ziz_izi​的值如何求出答案

我们将ppp粘在sss的前面,中间用一个字符(如下划线)隔开,可以得到一个字符串sss。我们假设我们已经知道了所有zi(s)z_i(s)zi​(s)的值,那么怎么求出答案呢。

我们可以扫描sss串中从p+2|p|+2∣p∣+2一直到p+t+1|p|+|t|+1∣p∣+∣t∣+1的位置iii,也就是原来的ttt字符串的位置,然后判断zi(s)z_i(s)zi​(s)是否等于ppp字符串的长度。如果等于,那么在以ttt字符串的这个位置就可以匹配ppp字符串。

为什么这个方法是正确的?

首先,根据zi(s)z_i(s)zi​(s)的定义,它表示以iii开头的子串和sss的最长公共前缀的长度。我们知道,sss是由ppp粘在ttt的前面得到的,因此sss的前缀实际上就是ppp字符串,而又因为我们ppp和ttt之间用一个字符隔开,所有zi(s)z_i(s)zi​(s)的值永远不可能大于p|p|∣p∣。这样我们就知道如果zi(s)=pz_i(s)=|p|zi​(s)=∣p∣就表示匹配成功。

3.怎样求出ziz_izi​

我们首先定义一个boxibox_iboxi​为区间[i,i+zi1][i,i+z_i-1][i,i+zi​−1]。我们假设当前我们在计算zi(s)z_i(s)zi​(s)的值,那么我们定义[l,r][l,r][l,r]为box2,box3,,boxi1box_2,box_3,\dots,box_{i-1}box2​,box3​,…,boxi−1​中最靠右的boxboxbox的左右端点。

那么有以下几种情况:

1.若i>ri \gt ri>r,则证明前面的所有的boxjbox_jboxj​和我们没有任何关联,我们无法利用,同时也证明boxibox_iboxi​一定是最靠右的,更新l=il=il=i,r=i+zi1r=i+z_i-1r=i+zi​−1,暴力匹配就行了。

2.若iri \leq ri≤r,则令i=il+1i'=i-l+1i′=i−l+1,因为iii位于[l,r][l,r][l,r]内,则我们知道sl,sl+1,,srs_l,s_{l+1},\dots,s_rsl​,sl+1​,…,sr​应该与s1,s2,,srs_1,s_2,\dots,s_rs1​,s2​,…,sr​匹配。因此我们可以将[l,r][l,r][l,r]平移到[1,rl+1][1,r-l+1][1,r−l+1]上去,ii'i′其实就是iii在这段区间中的对应位置。因此我们可以根据zi(s)z_{i'}(s)zi′​(s)的数值来计算zi(s)z_i(s)zi​(s),我们令β=ri+1\beta=r-i+1β=r−i+1。

下面我们又可以分出两种情况:

(1)(1)(1) zi(s)&lt;βz_{i&#x27;}(s)&lt;\betazi′​(s)<β。即boxibox_{i&#x27;}boxi′​这个位置控制的右端点并没有超过rrr,直接令zi=ziz_i=z_{i&#x27;}zi​=zi′​,l,rl,rl,r保持原样。

(2)(2)(2) zi(s)βz_{i&#x27;}(s)\geq\betazi′​(s)≥β。boxibox_{i&#x27;}boxi′​的右端点超过了超过了[l,r][l,r][l,r]对应的前缀。因为我们仅仅知道sl,sl+1,,srs_l,s_{l+1},\dots,s_rsl​,sl+1​,…,sr​与s1,s2,,srs_1,s_2,\dots,s_rs1​,s2​,…,sr​匹配,还不知道后面的部分。因此我们更新l=il=il=i,继续暴力匹配后面的长度,匹配完成后更新zi=rlz_i=r-lzi​=r−l。

4.时间复杂度

我们发现rrr是单调递增的,因此时间复杂度也是线性的。(类似于two pointerstwo\ pointerstwo pointers)。

5.完整代码

ZAlgorithmZ-AlgorithmZ−Algorithm的完整代码:

#include <bits/stdc++.h>
using namespace std;
int n,z[200005];
char t[100005],p[100005],x[200005];
inline void Z_algorithm(){
	memset(z,0,sizeof(z));
	int l=0,r=0;
	for(int i=1;i<n;i++){
		if(i<=r)	z[i]=min(z[i-l],r-i);
		while(i+z[i]<n&&x[z[i]]==x[i+z[i]])	z[i]++;
		if(i+z[i]>r){
			l=i;
			r=i+z[i];
		}
	}
}
int main(){
	scanf("%s%s",t+1,p+1);
	for(int i=1;i<=strlen(p+1);i++)	x[n++]=p[i];
	x[n++]='_';
	for(int i=1;i<=strlen(t+1);i++)	x[n++]=t[i];
	Z_algorithm();
	vector<int> ans;
	for(int i=strlen(p+1)+1;i<n;i++)
		if(z[i]==strlen(p+1))
			ans.push_back(i-strlen(p+1));
	for(int i=0;i<ans.size();i++)	printf("%d ",ans[i]);
}

6.例题

Codeforces 126BPasswordCodeforces\ 126B - PasswordCodeforces 126B−Password

思路:扫一遍,判断是否合法即可。ZalgorithmZ-algorithmZ−algorithm的基础题,建议没接触过ZalgorithmZ-algorithmZ−algorithm的先做这道题。

#include <bits/stdc++.h>
using namespace std;
char s[1000005];
int n,z[1000005];
inline void Z_algorithm(){
	int l=0,r=0;
	for(int i=1;i<n;i++){
		if(i<=r)	z[i]=min(z[i-l],r-i);
		while(i+z[i]<n&&s[i+z[i]]==s[z[i]])	z[i]++;
		if(i+z[i]>r){
			l=i;
			r=i+z[i];
		}
	}
}
int main(){
	scanf("%s",s);
	n=strlen(s);
	Z_algorithm();
	int maxx=0,pos=0;
	for(int i=1;i<n;i++){
		if(z[i]==n-i&&maxx>=n-i){
			pos=i;
			break;
		}
		maxx=max(maxx,z[i]);
	}
	if(!pos)	puts("Just a legend");
	else
		for(int i=0;i<n-pos;i++)
			putchar(s[i]);
}

Codeforces 427DMatch&amp;CatchCodeforces\ 427D - Match\&amp;CatchCodeforces 427D−Match&Catch

思路:将sss的每个后缀接在sss的前面,用一个字符隔开。再将得到的字符串接在ttt的前面,用一个字符隔开,对这个字符串进行ZalgorithmZ-algorithmZ−algorithm,求出ziz_izi​中最大值和次大值,判断一下就行了。

#include <bits/stdc++.h>
using namespace std;
int n,m,tot;
char s[5005],t[5005],x[15005];
int z[15005],ans=INT_MAX;
inline void Z_algorithm(){
	memset(z,0,sizeof(z));
	int l=0,r=0;
	for(int i=1;i<tot;i++){
		if(i<=r)	z[i]=min(z[i-l],r-i);
		while(i+z[i]<tot&&x[z[i]]==x[i+z[i]])	z[i]++;
		if(i+z[i]>r){
			l=i;
			r=i+z[i];
		}
	}
}
int main(){
	scanf("%s%s",s+1,t+1);
	n=strlen(s+1),m=strlen(t+1);
	for(int i=1;i<=n;i++){
		memset(x,0,sizeof(x));tot=0;
		for(int j=i;j<=n;j++)	x[tot++]=s[j];
		x[tot++]='$';
		for(int j=1;j<=n;j++)	x[tot++]=s[j];
		x[tot++]='#';
		for(int j=1;j<=m;j++)	x[tot++]=t[j];
		Z_algorithm();
		z[n+1]=0;
		int f=0,g=0,idx=-1;
		bool good=false;
		for(int j=1;j<tot;j++){
			if(z[j]>f){
				g=f;
				f=z[j];
				idx=j;
				good=1;
			}
			else if(z[j]==f)	good=0;
			else if(z[j]>g)		g=z[j];
		}
		if(good&&idx>2*n-i+1)	ans=min(ans,g+1);
	}
	if(ans==INT_MAX)	puts("-1");
	else				cout<<ans<<endl;
}

Codeforces 526DOmNomandNecklaceCodeforces\ 526D - Om Nom and NecklaceCodeforces 526D−OmNomandNecklace

思路:对整个字符串进行一次ZalgorithmZ-algorithmZ−algorithm,然后枚举ABA-BA−B段的长度,判断一下即可。

#include <bits/stdc++.h>
using namespace std;
int n,k,a[1000005],z[1000005],sum=0;
char x[1000005];
inline void Z_algorithm(){
	memset(z,0,sizeof(z));
	int l=0,r=0;
	for(int i=1;i<n;i++){
		if(i<=r)	z[i]=min(z[i-l],r-i);
		while(i+z[i]<n&&x[z[i]]==x[i+z[i]])	z[i]++;
		if(i+z[i]>r){
			l=i;
			r=i+z[i];
		}
	}
}
int main(){
	scanf("%d%d%s",&n,&k,x);
	Z_algorithm();
	for(int len=1;len*k<=n;len++){
		if(z[len]>=len*(k-1)){
			a[len*k]++;
			a[min(len*k+len,len+z[len])+1]--;
		}
	}
	for(int i=1;i<=n;i++){
		sum+=a[i];
		if(sum>0)	putchar('1');
		else		putchar('0');
	}
}

Codeforces 1200ECompressWordsCodeforces\ 1200E - Compress WordsCodeforces 1200E−CompressWords

话说这是666天前刚比完的一场Div.2Div.2Div.2的EEE

思路:对于每一个字符串,以它作为模式串ppp,并取当前答案的后min(ans.size(),s[i].size())min(ans.size(),s[i].size())min(ans.size(),s[i].size())位所为文本串ttt,进行ZalgorithmZ-algorithmZ−algorithm,最后判断一下是否有位置iii使得i+zi=t.size()i+z_i=t.size()i+zi​=t.size()即可。

#include <bits/stdc++.h>
using namespace std;
int n,z[1000005];
string s[100005],ans,x;
inline void Z_algorithm(){
	int l=0,r=0;
	for(int i=1;i<x.size();i++){
		z[i]=0;
		if(i<=r)	z[i]=min(z[i-l],r-i);
		while(i+z[i]<x.size()&&x[i+z[i]]==x[z[i]])	z[i]++;
		if(i+z[i]>r){
			l=i;
			r=i+z[i];
		}
	}
}
int main(){
	scanf("%d",&n);
	for(int i=0;i<n;i++){
		cin>>s[i];
		x=s[i]+"_"+ans.substr(ans.size()-min(ans.size(),s[i].size()));
		Z_algorithm();
		int maxx=0;
		for(int j=s[i].size()+1;j<x.size();j++)
			if(z[j]==x.size()-j)
				maxx=max(maxx,z[j]);
		ans+=s[i].substr(maxx);
	}
	for(int i=0;i<ans.size();i++)	putchar(ans[i]);
}

7.更多练习题

ZalgorithmZ-algorithmZ−algorithm的更多练习题,大家有时间尽量做哦:

CF119D String Transformation
CF631D Messenger
CF535D Tavas and Malekas
CF955D Scissors
CF149E Martian Strings
上一篇:单片机爬坑记-02-资源紧缺


下一篇:极光客户互动云java post请求