简介
对于一个长度为 (n) 的字符串 (s),定义函数 (z[i]) 表示 (s) 和 (s[i,n-1]) (即以 (s[i]) 开头的后缀)的最长公共前缀(LCP)的长度。(z) 被称作为 (s) 的 Z 函数。特别地,(z[0]=[0])。
国外一般将计算该数组的算法称为 Z Algorithm,而国内则称其为 扩展 KMP。
样例
下面若干样例展示了对于不同字符串的 Z 函数:
- (z( ext{aaaaa})=[0,4,3,2,1])
- (z( ext{aaabaab})=[0,2,1,0,2,1,0])
- (z( ext{abacaba})=[0,0,1,0,3,0,1])
线性算法
在该算法中,我们从 (1) 到 (n−1) 顺次计算 (z[i]) 的值((z[0]=0))。在计算 (z[i]) 的过程中,我们会利用已经计算好的 (z[0],…,z[i−1])。
对于 (i),我们称区间 ([i,i+z[i]−1]) 是 (i) 的 匹配段,也可以叫 Z-box。
举例:
对于 (z( ext{abacaba})=[0,0,1,0,3,0,1]:)
- (i=2) 时,匹配段为 ([2,2]),即字符
a
。 - (i=4) 时,匹配段为 ([4,6]),即子串
abc
。
算法的过程中我们维护右端点最靠右的匹配段。为了方便,记作 ([l,r])。根据定义,(s[l,r]) 是 (s) 的前缀。在计算 (z[i]) 时我们保证 (l≤i)。初始时 (l=r=0)。
在计算 (z[i]) 的过程中:
- 如果 (i≤r),那么根据 ([l,r]) 的定义有 (s[i,r]=s[i−l,r−l]),因此 (z[i]≥min(z[i−l],r−i+1))。这时:
- 若 (z[i−l]<r−i+1),则 (z[i]=z[i−l])。
- 否则 (z[i−l]≥r−i+1),这时我们令 (z[i]=r−i+1),然后暴力枚举下一个字符扩展 ([i]) 直到不能扩展为止。
- 如果 (i>r),那么我们直接按照朴素算法,从 (s[i]) 开始比较,暴力求出 (z[i])。
- 在求出 (z[i]) 后,如果 (z[i]>r),我们就需要更新 ([l,r]),即令 (l=i,r=i+z[i]−1)。
代码
设小串(模式串)为 (B),大串(文本串)为 (A)。
本代码中 (z[0]=|B|)。
求 (z) 数组:
void getZ()
{
z[0] = lenb;
int now = 0;
while (now + 1 < lenb && B[now] == B[now + 1])
now++;
z[1] = now;
int p0 = 1;
for (int i = 2; i < lenb; ++i)
{
if (i + z[i - p0] < p0 + z[p0])
{
z[i] = z[i - p0]; //第一种情况
}
else
{
now = p0 + z[p0] - i;
now = max(now, 0);
while (now + i < lenb && B[now] == B[now + i])
now++;
z[i] = now;
p0 = i;
}
}
}
求 (B) 与 (A) 的每一个后缀的 LCP 长度数组:
void exkmp()
{
int now = 0;
while (now < lenb && now < lena && B[now] == A[now])
now++;
ext[0] = now;
int p0 = 0;
for (int i = 1; i < lena; ++i)
{
if (i + z[i - p0] < p0 + ext[p0])
ext[i] = z[i - p0];
else
{
now = p0 + ext[p0] - i;
now = max(now, 0);
while (now < lenb && now + i < lena && B[now] == A[now + i])
now++;
ext[i] = now;
p0 = i;
}
}
}
复杂度分析
对于内层 while
循环,每次执行都会使得 (r) 向后移至少 (1) 位,而 (r<n−1),所以总共只会执行 (n) 次。
对于外层循环,只有一遍线性遍历。
总复杂度为 (O(n))。