Z 函数(扩展KMP)
简介
定义一个长度为 \(n\) 的字符串 \(s\),定义 \(z_i\) 表示 \(s\) 和 \(s[i,n]\) 的 \(lcp\)(最长公共前缀),称 \(z\) 为 \(s\) 的 \(Z\) 函数。
朴素算法
暴力枚举,时间复杂度:\(O(n^2)\)
线性算法
我们首先考虑顺次处理 \(z_1,z_2,\dots,z_n\),当枚举到 \(i\) 时,也就是说 \(z_1,\dots,z_{i-1}\) 是已经求得了。
我们再来定义名词 Z-box :指与字符串 \(s\) 匹配的前缀区间,同时满足包含 \(i\) (当枚举到 \(i\) 时)且右端点最大。
首先我们来考虑如何更新这个 Z-box (分类讨论):
-
设 \(l\) 为左端点(\(i-1\) 的 Z-box),\(r\) 为右端点(\(i-1\) 的 Z-box),\(i\) 为当前枚举的点。
-
当 \(i>r\) 时,那也就是上一个 Z-box 对于 \(i\) 时没有用处的,那就从 \(i\) 开始,暴力向后匹配求得 Z-box
-
当 \(i\le r\) 时,分两种情况:
-
\(z_{i-l+1}<r-l+1\)
那么 \(s[1,z[i-l+1]]=s[i-1+1,i-l+z[i-l+1]]=s[i,i+z[i-l+1]-1]\) (图标可能有点问题,以公式为准!!!)
-
\(z_{i-l+1}\ge r-l+1\)
那么 \(s[1,r-l+1]=s[l,r]\),但是后面的匹配不能保证所以就向后匹配即可。
-
时间复杂度证明
枚举 \(i\) 是 \(O(n)\) 的,\(r\) 会一直向后移动也是 \(O(n)\) 的,所以总的来说是 \(O(n)\) 的。
代码实现
inline void getz()
{
for(int i=1;i<=m;i++)z[i]=0;
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(i+z[i]<=m&&b[i+z[i]]==b[z[i]+1])++z[i];
if(r<i+z[i]-1)l=i,r=i+z[i]-1;
}
}
同时这个也可拓展到两个串的匹配(\(s\) 与 \(t\) 的每一个后缀的 \(lcp\)),代码实现:
inline void getext()
{
for(int i=1;i<=n;i++)ext[i]=0;
for(int i=1,l=0,r=0;i<=n;i++)
{
if(i<=r)ext[i]=min(z[i-l+1],r-i+1);
while(i+ext[i]<=n&&b[ext[i]+1]==a[i+ext[i]])++ext[i];
if(r<i+ext[i]-1)l=i,r=i+ext[i]-1;
}
}