扩展 KMP
简介
对于一个长度为 \(n\) 的字符串 \(s\),定义函数 \(z[i]\) 表示 \(s\) 和 \(s[i,n-1]\) (即以 \(s[i]\) 开头的后缀)的最长公共前缀(LCP)的长度。\(z\) 被称作为 \(s\) 的 Z 函数。特别地,\(z[0]=[0]\)。
国外一般将计算该数组的算法称为 Z Algorithm,而国内则称其为 扩展 KMP。
样例
下面若干样例展示了对于不同字符串的 Z 函数:
- \(z(\text{aaaaa})=[0,4,3,2,1]\)
- \(z(\text{aaabaab})=[0,2,1,0,2,1,0]\)
- \(z(\text{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(\text{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)\)。

浙公网安备 33010602011771号