Skip to content

定义Definition

Z 函数

施工中 · 内容逐步修订

Z-function · Z-array

记录每个后缀与整个字符串前缀的最长公共前缀长度,并通过最右匹配区间在线性时间计算的数组。

形式陈述 ​

设 s 长度为 n,采用从零开始的下标和左闭右开区间。对 1≤i<n,

Z[i]=max{k:0≤k≤n−i, s[0:k]=s[i:i+k]}.

本文约定 Z[0]=0;也有资料约定 Z[0]=n,其余位置没有区别。空串的 Z 数组为空。算法只要求元素可作常数时间相等比较,所以同样适用于整数等组成的有限词。[1][2]

一张数组的含义 ​

取 s = aabcaabxaaaz:

下标 i 0 1 2 3 4 5 6 7 8 9 10 11
s[i] a a b c a a b x a a a z
Z[i] 0 1 0 0 3 1 0 0 2 2 1 0

从位置 4 开始的 aabx… 与前缀 aabc… 有三个字符相同,所以 Z[4]=3。位置 8 的 aaaz 则只匹配前缀 aabc 的前两位,因此 Z[8]=2。数组记录的是匹配长度,不是失配字符的位置。

逐个位置从头比较当然能得到正确结果,但对 aaaa…a,比较次数为 (n−1)+(n−2)+⋯+1=Θ(n2)。线性算法要避免反复确认已经知道相同的区段。

直觉

把前缀移到每个位置,能对上多长 ​

给定字符串 s,把它的整个前缀与从位置 i 开始的后缀对齐,从左到右比较,直到第一次失配或后缀结束。匹配的长度就是 Z[i]。Z 函数把这件事对所有位置同时做好,因而能快速回答“这个位置是否出现了开头那一段”。

Z-box:让已有匹配替下一次比较作证 ​

从左到右处理位置,维护一个已经算出的匹配区间 [l,r),满足

s[l:r]=s[0:r−l],

并使右端点 r 在已处理位置形成的区间中最靠右。这个区间称为 Z-box。

若 i≥r,当前没有覆盖 i 的可用匹配信息,就从长度 0 开始比较。若 i<r,位置 i 在 box 内对应前缀中的位置 k=i−l,因此至少可以复制

Z[i]≥min{Z[k],r−i}

这一段已知相等的信息。这里必须取最小值:Z[k] 可能延伸到旧 box 覆盖不到的地方,那部分尚未被证实。

接下来分两种情况理解。若 Z[k]<r−i,导致 Z[k] 停止的失配也落在 box 内,便有 Z[i]=Z[k],答案已经确定。若 Z[k]≥r−i,目前只能确认一直匹配到 r,需要从边界继续向右试。得到完整答案后,若 i+Z[i]>r,就令 l=i、r=i+Z[i]。

在上例中,位置 4 建立 [4,7)。处理 i=5 时,复制 Z[1]=1,而 1<7−5,所以已有失配足以确定 Z[5]=1。处理 i=9 时,旧 box 是 [8,10),只能先复制到边界的一位;再比较发现下一位也相同,才能把答案延长为 2。

Z-box、镜像位置与区间复用
例子与边界

最小实现与线性时间证明 ​

下面把两种情况合并到同一段代码中;已知 box 内会失配时,while 的第一次比较会立即停止。

python
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 内的失配也可靠;边界外的新比较则把答案补到首次失配。每次更新 [l,r) 都由一次实际匹配支持,不变量随之保持。

复杂度不能用“每次比较都推进右端点”来证明,因为失配比较并不推进任何东西。准确的记账是:成功的新比较只发生在旧右边界及其右侧,每成功一次就把当前已探索的边界推进一位,全部迭代合计至多 n−1 次;每个外层位置至多再产生一次失配比较。于是字符比较总数为 O(n),时间为 O(n),结果数组占 O(n) 空间。[1]

Manacher 算法在回文问题中采用相近的边界复用记账,但复用的是中心镜像半径,既不是前缀匹配长度,也不能直接套用本页的镜像下标。其奇、偶中心各自维护已证实回文,成功扩张与每中心至多一次的失配分别收费。

推论与应用

匹配、border 和周期 ​

对非空模式 P 与文本 T,构造 P#T,其中 # 不出现在二者中。令 m=|P|;拼接串中位置 i≥m+1 满足 Z[i]≥m,当且仅当文本位置 i−m−1 开始出现一次完整的 P。因为分隔符阻断继续匹配,这些位置实际上有 Z[i]=m。例如 aba#ababa 会报告文本中的位置 0 和 2,重叠出现也不会漏掉。若字符表没有可用分隔符,可先把字符编码为整数,再使用一个表外标记。

长度 b 是整个字符串的非空真 border,当且仅当 1≤b<n 且 Z[n−b]=b。这里 border 指既是前缀又是后缀的同一段内容。类似地,1≤p<n 是一个周期,当且仅当 Z[p]=n−p:这恰好检查了所有 s[j]=s[j+p]。周期长度不必整除 n;要判断字符串是否由同一块整数次重复组成,还需 p∣n。

周期与 border将本段两个判据连成位移与重合长度的完整对应。利用 Z 数组核实两个周期后,仍不能无条件取它们的最大公约数:Fine–Wilf 引理给出所需长度,长度六的 abaaba 则说明周期三、五并不自动带来周期一。

前缀函数 π[i] 以“当前位置结尾的前缀”为对象,Z 函数以“当前位置开头的后缀”为对象。两者的数组一般不相等,但在有效数组之间可以在线性时间互相转换;这里的“等价”指前缀重合信息可相互恢复,不表示逐项相等,也不允许混用下标公式。

参考资料

[1] Dan Gusfield,Algorithms on Strings, Trees, and Sequences,Cambridge University Press,1997,第 1 章:Z 值、线性预处理与模式匹配。

[2] cp-algorithms contributors,Z-function:半开区间实现、不变量与应用;本文代码与例子采用统一的零下标约定。

关系图谱7 个相邻概念 · 3 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系

被这些条目使用

限定层次等价