Z函数

Z 函数是的意义是对于字符串的后缀 \(i\),其最长的前缀使得存在原串的一个前缀和它相同。

我个人认为 Z 函数是简单于 KMP 的,因为 KMP 的思想是利用前面的答案递归调用计算新的位置,而 Z 函数是简单的递推,只需要一个原先计算的结果就能得出答案,不需要递归。

Z 函数的核心思想是匹配段思想,我们一个 \([i,i+z_i-1]\) 称为一个匹配段。而当我们递推计算 Z 函数的时候,我们总是找到当前右端点最靠右的匹配段。

image

然后我们考虑最右匹配段能给我们提供什么信息。它告诉我们 \([1,r-l+1]\)\([l,r]\) 是相同的。那么对于 \(j=r-l+1-(r-i)=i-l+1\),有 \([j,r-l+1]\)\([i,r]\) 相同。

所以,如果 \(z_j<r-l-z_j+2\),那么意味着 \([j,j+z_j-1]\)\([1,z_j]\) 匹配并且 \(j+z_j\)\(z_j+1\) 不匹配。那么对于 \(z_i\),这也成立。\(z_i=z_j\)

image

然后,如果 \(z_j\ge r-l-z_j+2\),我们就能确定 \([j,r-l+1]\) 就是匹配的,\([i,r]\) 是匹配的。但是因为 \([i,r]\) 后面的部分不一定和 \(r-l+1\) 后面的匹配,所以我们最多直接套用 \(r-i+1\) 的长度,后面的都需要暴力扩展。

image

我们观察发现,每次暴力扩展,一定在 \(r\) 之后进行。也就是每次暴力扩展至少将最右匹配段增加 \(1\),所以暴力扩展的总次数不会超过 \(O(n)\)

然后,我们就得到了 Z 函数的完整计算方法。

inline void buildz(){
	int l=1,r=0;z[1]=0;
	rp(i,n)z[i]=0;
	rep(i,2,n){
		if(r>=i)z[i]=max(0,min(r-i+1,z[i-l+1]));
		while(i+z[i]<=n&&s[i+z[i]]==s[z[i]+1])z[i]++;
		if(z[i]>r)l=i,r=i+z[i]-1;
	}
}
posted @ 2023-05-30 10:21  jucason_xu  阅读(444)  评论(0编辑  收藏  举报