[字符串学习笔记] 3. Z 函数(扩展 KMP 算法)
3.1. 定义
对于字符串 \(s\),定义:
-
\(s\) 与以 \(s[i]\) 为首的后缀的 最长公共前缀 被称作 \(i\) 的 Z-box。形式化地,即 \(\operatorname{lcp}(s, s[i \ldots |s| - 1])\)。
-
\(z[i]\) 为 \(i\) 的 Z-box 的长度,特别地,\(z[0] = 0\)。这个序列被称作 \(s\) 的 Z 函数。
例如,\(\texttt{abacabab}\) 的 Z 函数序列为 \([0, 0, 1, 0, 3, 0, 2, 0]\),而 \(\texttt{aaaaa}\) 的 Z 函数序列为 \([0, 4, 3, 2, 1]\)。
计算 \(z\) 序列的算法被称作 扩展 KMP 算法 或 exKMP 算法。
3.2. 实现
3.2.1. 朴素实现
从 \(1\) 到 \(|s| - 1\) 计算 \(z[i]\)。一开始,\(z[i] = 0\),循环判断直到 \(i + z[i] > |s| - 1\) 或 \(s[z[i]] \neq s[i + z[i]]\) 为止。
时间复杂度 \(\Theta({|s|}^2)\)。
3.2.2. 优化
在计算 \(z[i]\) 的过程中,考虑 Z-box 区间 \([l, r]\),满足在 \(l \leq i\) 时 \(r\) 尽可能大。初始化 \(l = r = 0\)。
若当前 \(i \leq r\),根据 Z 函数的定义,有 \(s[i - l \ldots r - l] = s[i \ldots r]\)。那么,
- 如果 \(z[i - l] \leq r - i + 1\),则 \(z[i] = z[i - l]\)。
- 否则,从 \(r - i + 1\) 开始,用朴素算法计算。
而若当前 \(i > r\),也是按照朴素算法计算。
最后,当 \(i + z[i] - 1 > r\) 时,更新 \([l, r] \gets [i, i + z[i] - 1]\)。
示例
考虑 \(s = \texttt{ababa}\) 这个例子。
初始状态下,\(i = 1\),\(l = r = z[0] = 0\)。
此时 \(i > r\),使用朴素算法计算得 \(z[1] = 0\)。
此时 \(i > r\),使用朴素算法计算得 \(z[2] = 3\)(\(s[0 \ldots 2] = \texttt{aba} = s[2 \ldots 4]\))。更新 \([l, r]\)。
此时 \(i \leq r\) 且 \(z[i - l] = z[1] < r - i + 1\),所以 \(z[3] = z[1] = 0\)。
此时 \(i \leq r\) 且 \(z[i - l] = z[2] \geq r - i + 1\),从 \(r - i + 1 = 1\) 开始朴素计算,得 \(z[5] = 1\)。
3.2.3. 线性算法实现
时间复杂度 \(\Theta(n)\)。
vector<int> getz(string s) {
vector<int> z(s.size()); // z[0] = 0
for (int i = 1, l = 0, r = 0; i < s.size(); i++) {
if (i <= r && z[i - l] < r - i + 1)
z[i] = z[i - l];
else {
z[i] = i <= r ? r - i + 1 : 0;
while (i + z[i] < s.size() && s[z[i]] == s[i + z[i]])
z[i]++;
}
if (i + z[i] - 1 > r) {
l = i;
r = i + z[i] - 1;
}
}
return z;
}
3.3. 字符串匹配
给定模式串 \(t\) 与待匹配串 \(s\),找出 \(t\) 在 \(s\) 中出现的所有位置。默认 \(|t| < |s|\)。
与 KMP 算法类似地,构造字符串 \(p = t + \texttt \# + s\)。
同理,考虑分隔符 \(\texttt \#\) 后 \(p\) 的 Z 函数 \(z[i]\) 的意义。很明显,仅有 \(|t| + 1 \leq i \leq |p| - |t|\) 时,\(z[i]\) 才有可能 \(= |t|\),此时 \(p[i \ldots i + |t| - 1]\) 也就是 \(t\) 在 \(s\) 所 对应的 出现位置。不难发现,\(t\) 出现的下标对应 \(i - |t| - 1\)。
时间复杂度 \(\Theta(|p|) = \Theta(|s| + |t|)\)。
3.4. 使用 Z 函数寻找 Border
根据定义,\(s\) 与以 \(s[i]\) 为首的后缀的 最长公共前缀 为 \(i\) 的 Z-box。如果 \(s[i \ldots |s| - 1]\) 匹配的最长公共前缀足够长,以至于其右端点位于 \(s\) 的末尾,那么 \(s[0 \ldots z[i] - 1] = s[0 \ldots |s| - i - 1] \in \operatorname{Border}(s)\)。
例题 SP21360 SUFEQPRE - Suffix Equal Prefix
题解
模版题。对于每个符合要求的 \(i\),累加答案即可。
时间复杂度 \(\Theta(|s|)\)。
void solve(int tc) {
cin >> s;
vector<int> z = getz(s);
int res = 0;
for (int i = 1; i < s.size(); i++)
if (z[i] == s.size() - i)
res++;
cout << "Case " << tc << ": " << res << '\n';
}

浙公网安备 33010602011771号