Z 函数是的意义是对于字符串的后缀 \(i\),其最长的前缀使得存在原串的一个前缀和它相同。
我个人认为 Z 函数是简单于 KMP 的,因为 KMP 的思想是利用前面的答案递归调用计算新的位置,而 Z 函数是简单的递推,只需要一个原先计算的结果就能得出答案,不需要递归。
Z 函数的核心思想是匹配段思想,我们一个 \([i,i+z_i-1]\) 称为一个匹配段。而当我们递推计算 Z 函数的时候,我们总是找到当前右端点最靠右的匹配段。
然后我们考虑最右匹配段能给我们提供什么信息。它告诉我们 \([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\)。
然后,如果 \(z_j\ge r-l-z_j+2\),我们就能确定 \([j,r-l+1]\) 就是匹配的,\([i,r]\) 是匹配的。但是因为 \([i,r]\) 后面的部分不一定和 \(r-l+1\) 后面的匹配,所以我们最多直接套用 \(r-i+1\) 的长度,后面的都需要暴力扩展。
我们观察发现,每次暴力扩展,一定在 \(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;
}
}
标签:匹配段,暴力,扩展,KMP,我们,函数
From: https://www.cnblogs.com/jucason-xu/p/17442525.html