“Z 函数提供另一种检查方式。对 $1\le p<n$,$Z[p]=n p$ 当且仅当 $p$ 是周期,因为这正好比较 $s[p:n]$ 和等长前缀。已拥有 Z 数组时,没有必要为了同一查询重…”
形式陈述
设
本文约定
一张数组的含义
取 s = aabcaabxaaaz:
| 下标 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| a | a | b | c | a | a | b | x | a | a | a | z | |
| 0 | 1 | 0 | 0 | 3 | 1 | 0 | 0 | 2 | 2 | 1 | 0 |
从位置 aabx… 与前缀 aabc… 有三个字符相同,所以 aaaz 则只匹配前缀 aabc 的前两位,因此
逐个位置从头比较当然能得到正确结果,但对 aaaa…a,比较次数为
直觉
把前缀移到每个位置,能对上多长
给定字符串
Z-box:让已有匹配替下一次比较作证
从左到右处理位置,维护一个已经算出的匹配区间
并使右端点
若
这一段已知相等的信息。这里必须取最小值:
接下来分两种情况理解。若
在上例中,位置
例子与边界
最小实现与线性时间证明
下面把两种情况合并到同一段代码中;已知 box 内会失配时,while 的第一次比较会立即停止。
def z_function(s: str) -> list[int]:
"""返回 Z 数组,采用 Z[0] = 0 和半开区间 [left, right)。"""
n = len(s)
z = [0] * n
left = right = 0
for i in range(1, n):
if i < right:
z[i] = min(right - i, z[i - left])
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1
if i + z[i] > right:
left, right = i, i + z[i]
return z
assert z_function('aabcaabxaaaz') == [0, 1, 0, 0, 3, 1, 0, 0, 2, 2, 1, 0]
正确性的关键是 box 内的字符串与前缀完全相同,因此复制给出的下界可靠,box 内的失配也可靠;边界外的新比较则把答案补到首次失配。每次更新
复杂度不能用“每次比较都推进右端点”来证明,因为失配比较并不推进任何东西。准确的记账是:成功的新比较只发生在旧右边界及其右侧,每成功一次就把当前已探索的边界推进一位,全部迭代合计至多
Manacher 算法在回文问题中采用相近的边界复用记账,但复用的是中心镜像半径,既不是前缀匹配长度,也不能直接套用本页的镜像下标。其奇、偶中心各自维护已证实回文,成功扩张与每中心至多一次的失配分别收费。
推论与应用
匹配、border 和周期
对非空模式 P#T,其中 # 不出现在二者中。令 aba#ababa 会报告文本中的位置
长度
周期与 border将本段两个判据连成位移与重合长度的完整对应。利用 Z 数组核实两个周期后,仍不能无条件取它们的最大公约数:Fine–Wilf 引理给出所需长度,长度六的 abaaba 则说明周期三、五并不自动带来周期一。
前缀函数
参考资料
[1] Dan Gusfield,Algorithms on Strings, Trees, and Sequences,Cambridge University Press,1997,第 1 章:Z 值、线性预处理与模式匹配。
[2] cp-algorithms contributors,Z-function:半开区间实现、不变量与应用;本文代码与例子采用统一的零下标约定。