Z 算法

Jun 24, 2023
blog CP

算法简介

\(Z\) 算法在国内也被某些人称为 \(\rm ExKMP\)(虽然我不知道它跟 \(\rm KMP\) 算法有什么关系)。

\(Z\) 算法用于求字符串 \(S\) 的 \(Z\) 函数,其定义为 \(z_{i}=\rm LCP(S,S_{i\sim n})\)。

这个算法的核心在于利用之前的信息来减少计算量。

具体来说我们可以从左往右依次求每个位置的 \(z\) 函数,令 \([i, i + z_{i} - 1]\) 为 \(i\) 的匹配段,同时维护出右端点最靠右的匹配段 \([l, r]\)。

那么我们知道有 \(S_{1 \sim r - l + 1} = S_{l \sim r}, S_{i \sim r} = S_{i - l + 1, r - l + 1}(i \leqslant r), z_{i} \geqslant \min(r - i + 1, z_{i - l + 1})\)

在计算 \(z_{i}\) 时,分以下三种情况:

  • 若 \(i \leqslant r, z_{i - l + 1} \lt r - i + 1\) 那么显然有 \(z_{i} = z_{i - l + 1}\)。
  • 若 \(i \leqslant r, z_{i - l + 1} \geqslant r - i + 1\),那么我们就从 \(r + 1\) 开始暴力匹配前缀。
  • 若 \(i \gt r\),那么直接从 \(i\) 开始暴力匹配前缀。

不难发现复杂度来源在于 \(2, 3\) 情况,但可以发现右端点最靠右的匹配段的右端点总是在这两种情况下向右移动,而总移动量不超过 \(n\),因此复杂度是 \(\Theta(n)\) 的。

如果要求 \(z_{i} = \rm LCP(T, S_{i \sim n})\) 话,就类似于 \(\rm KMP\) 的做法,构造一个 \(T \# S\) 的串去求 \(z\) 函数即可。

经典例题

[NOI2014] 动物园

对于每个 \(i\),计算后缀 \(S_{i\sim n}\) 与 \(S\) 的 \(\rm LCP\),考虑在后一个 border 的左端点 \(i\) 处统计答案。

令最长公共前缀的四个端点分别为 \(L,R,L',R'\),分两种情况讨论:

  • 当 \(R\lt L'\) 时,区间 \([L',R']\) 答案 \(+1\)。
  • 当 \(R\geqslant L'\) 时,区间 \([L',R]\) 答案 \(+1\)。

区间 \(+1\) 可以使用差分实现,总时间复杂度 \(\Theta(n)\)。

[CF1051E] Vasya and Big Integers

记 \(f_{i}\) 表示 \(a_{1\sim i}\) 有多少种合法的划分方案,显然有一个 \(\Theta(n^{2})\) 的暴力 \(\rm DP\),即往前枚举转移点 \(j\),判断转移是否合法后累加。

观察到合法的转移点是一段连续的区间,若令 \(len_{l}\) 和 \(len_{r}\) 分别表示两限制字符串的长度,显然对于满足 \(len_{l}\lt i-j\lt len_{r}\) 的转移点 \(j\),这个转移都是合法的,现在的问题在于判断当 \(j=i-len_{l}\) 或 \(j=i-len_{r}\) 的时候转移是否合法。

考虑用 \(\rm ExKMP\) 对任意一个 \(i\),求 \(\rm LCP(s_{i\sim n},l)\) 和 \(\rm LCP(s_{i\sim n},r)\),这样就可以快速求出 \(s_{i-len_{l}+1\sim i}\) 与 \(l\) 的最长公共前缀 \(zl_{i-len_{l}+1}\),并通过比较 \(s_{i-len_{l}+1+z_{i-len_{l}+1}}\) 与 \(l_{z_{i-len_{l}+1}+1}\) 的大小判断当 \(j=i-len_{l}\) 时转移是否合法,同理,当 \(j=i-len_{r}\) 时转移是否合法也可以通过相同方式判断。

这样,合法转移点的区间就在 \(\Theta(1)\) 的时间内求出来了,剩下的用前缀和记录一下即可。需要注意的是,由于不能出现前导零,所以当转移点的后一位为 ‘\(0\)’ 时不能将其加入前缀和数组。

总时间复杂度 \(\Theta(n)\)。