[字符串学习笔记] 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\)

\[\small \begin{matrix} & &\overset i \downarrow & & & \\ \text{index} &0 &1 &2 &3 &4\\ s &\texttt a &\texttt b &\texttt a &\texttt b &\texttt a \\ z &0 & & & & \\ &\underset{l, r}\uparrow & & & & \end{matrix} \]

此时 \(i > r\),使用朴素算法计算得 \(z[1] = 0\)

\[\small \begin{matrix} & & &\overset i \downarrow & & \\ \text{index} &0 &1 &2 &3 &4\\ s &\texttt a &\texttt b &\texttt a &\texttt b &\texttt a \\ z &0 &0 & & & \\ &\underset{l, r}\uparrow & & & & \end{matrix} \]

此时 \(i > r\),使用朴素算法计算得 \(z[2] = 3\)\(s[0 \ldots 2] = \texttt{aba} = s[2 \ldots 4]\))。更新 \([l, r]\)

\[\small \begin{matrix} & & & &\overset i \downarrow & \\ \text{index} &0 &1 &2 &3 &4\\ s &\texttt a &\texttt b &\texttt a &\texttt b &\texttt a \\ z &0 &0 &3 & & \\ & & &\underset l \uparrow & &\underset r \uparrow \end{matrix} \]

此时 \(i \leq r\)\(z[i - l] = z[1] < r - i + 1\),所以 \(z[3] = z[1] = 0\)

\[\small \begin{matrix} & & & &&\overset i \downarrow \\ \text{index} &0 &1 &2 &3 &4\\ s &\texttt a &\texttt b &\texttt a &\texttt b &\texttt a \\ z &0 &0 &3 &0 & \\ & & &\underset l \uparrow & &\underset r \uparrow \end{matrix} \]

此时 \(i \leq r\)\(z[i - l] = z[2] \geq r - i + 1\),从 \(r - i + 1 = 1\) 开始朴素计算,得 \(z[5] = 1\)

\[\small \begin{matrix} \text{index} &0 &1 &2 &3 &4\\ s &\texttt a &\texttt b &\texttt a &\texttt b &\texttt a \\ z &0 &0 &3 &0 &1 \end{matrix} \]

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';
}
posted @ 2024-06-22 12:44  Carrot-Meow~  阅读(65)  评论(0)    收藏  举报