z 函数

定义

对于字符串 \(S\),令 \(z_i=\text{lcp}(S,\text{ssf}(S,i))\)\(z_1\) 一般设为 \(0\)\(n\)

也称为扩展 \(\text{KMP}\)

计算 \(\text{Z}\) 函数的网站

模板

namespace string_algo {

    // z 函数
    template <typename value_t, typename output_iter_t, ass_is_RAI(output_iter_t)>
        void z_function(const basic_string<value_t> &s, output_iter_t z){
            if (s.empty())return;
            size_t n = s.size();
            *z = 0;
            for (size_t i = 1, l = 0, r = 1; i != n; ++i){//[l,r)
                if (i < r && z[i - l] < r - i)z[i] = z[i - l];
                else {
                    if (i < r)z[i] = r - i; else z[i] = 0;
                    while (i + z[i] < n && s[z[i]] == s[i + z[i]])++z[i];
                }
                if (i + z[i] > r)l = i, r = i + z[i];
            }
        }

}

时间复杂度 \(O(n)\)

模板题:P5410 【模板】扩展 KMP/exKMP(Z 函数) \(\quad\) 代码

匹配子串

要找到字符串 \(p\) 在字符串 \(t\) 中所有出现的位置,构造 \(s=p+\square+t\),其中 \(\square\) 为一个在 \(t\)\(p\) 中都没有出现过的字符,计算出 \(s\)\(z\) 函数 \(z_{1\sim |s|}\),若 \(z_{|p|+1+i}=|p|\),则 \(t_{[i,i+|p|)}\)\(p\) 的一次出现

时空复杂度为 \(O(|p|+|t|)\)

字符串整周期

对于给定的字符串 \(s\),求出最短的字符串 \(t\) 使得存在正整数 \(k\)\(s=t^k\)

\(|t|\) 为最小的 \(i\) 使得 \(i+z_i=|s|\)

时空复杂度 \(O(|s|)\)

例 1:P7114 [NOIP2020] 字符串匹配

给定 \(S_{1\sim n}\),求出有多少方法将其表示为 \(S=(AB)^kC\) 的形式,满足 \(f(A)\le f(C)\),其中 \(i\ge 1\)\(A,B,C\) 为非空字符串,\(f(S)\) 表示 \(S\) 中出现奇数次字符的数量,\(n\le 2^{20}\)\(S_i\) 为小写字母,多测 \(T\le5\)

考虑枚举 \(|AB|\)

\(i\) 为奇数,则 \(f(C)=f(\text{sff}(S,|AB|+1))\),否则 \(f(C)=f(S)\)

由于 \(1\le i\le\left\lfloor\frac{\min(z_{|AB|},n-|AB|+1)}{|AB|}\right\rfloor+1\)\(z_{|AB|}\) 是因为需要有长为 \(|AB|\) 的整周期,\(n-|AB|+1\) 是因为 \(C\) 非空)

可以算出合法的 \(i\) 的范围中,\(i\) 为奇数的数量 \(Ct_1\) 和为偶数的数量 \(Ct_0\),容易 \(O(1)\) 处理

\(Fsf_i=f(\text{sff}(i))\)\(Fpr_i=f(\text{prf}(i))\),容易 \(O(n)\) 递推处理

则一个 \(|AB|\) 的贡献为 \(Ct_0\cdot Ft(Fsf_1)+Ct_1\cdot Ft(Fsf_{|AB|+1})\),其中 \(Ft(x)=\sum_{A=1}^{|AB|-1}[Fpr_A\le x]\)

\(Vl(x)=Ft(x)-Ft(x-1)\)\(Vl(0)=Ft(0)\)),则枚举的 \(|AB|\) 加一时,会令一个 \(Vl(k)\) 加一,且每次查询为 \(Vl\) 的前缀和

容易使用树状数组维护(显然 \(Fpr(x)\le V\),其中 \(V\) 为字符集大小 \(26\)

时间复杂度 \(O(\sum n\log V)\)

代码

例 2:[ARC058F] 文字列大好きいろはちゃん

\(n\) 个字符串 \(s_{1\sim n}\),选若干个按给出顺序连接,总长必须等于 \(k\),求字典序最小的,保证有解,\(1\le n\le 2000,k\le10^4,\sum |s_i|\le10^6\),时限 \(5s\)

考虑 \(dp\)

\(f_{i,j}\) 表示 \(s_{1\sim i}\) 中选出总长为 \(j\) 的字典序最小字符串(即 \(f_{i,j}\) 值为一个长为 \(j\)字符串

\(f_{i,j}=\min(f_{i-1,j},f_{i-1,j-|s_i|}+s_i)\)(其中 \(\min\) 为取字典序较小的,\(+\) 表示字符串拼接)

显然其中一部分 \(f_{i,j}\) 对答案无用(具体定义之后讲)

\(0/1\) 背包求出 \(g_{i,j}\),表示 \(\text{sff}(S,i)\) 中是否可能选出长为 \(j\) 的字符串

\(g_{i+1,k-j}=0\)\(f_{i,j}\) 为无用的

对于同一个 \(i\),若存在 \(j<k\)\(f_{i,j}\) 不为 \(f_{i,k}\) 前缀,则字典序较大的一个一定不优,若两者对应的 \(g\) 都不为 \(0\),则显然字典序较大的一个无用

因此对于同一 \(i\),其所有有用的 \(f_{i,j}\) 一定为 \(j\) 最大的一个的前缀

即令 \(res_i\)\(j\) 最大的一个有用的 \(f_{i,j}\),则有用的 \(f_{i,k}=\text{prf}(res_i,k)\)

这样空间复杂度降到 \(O(nk)\)

\(i-1\) 转移到 \(i\),考虑维护一个单调栈,栈中保存有用的 \(f_{i,j}\),且下方的为上方的前缀

从小到大枚举 \(j\)

若新加入一个 \(f_{i,j}\)(需要保证其满足对应的 \(g\) 不为 \(0\),且 \(f_{i-1,j}\)\(f_{i-1,j-|s_i|}\) 至少有一个有用,此时 \(f_{i,j}\) 可通过 \(f_{i-1,j}\)\(f_{i-1,j-|s_i|}\) 求出),则有三种情况:

  1. 栈空则直接压入栈
  2. 栈顶的 \(f_{i,j}\) 字典序小于当前处理的 \(f_{i,j}\),则当前 \(f_{i,j}\) 无用
  3. 否则一直弹栈(也可能不需要弹)直到栈空或栈顶的 \(f_{i,j}\) 为当前处理 \(f_{i,j}\) 的前缀,然后将其压入栈

计算完一个 \(i\) 后,栈顶即为其 \(res\)

最终 \(res_n\) 即为答案

这样 暴力计算 可以求出所有有用的 \(f_{i,j}\),虽然时间复杂度和直接计算相同,但可以进一步优化

瓶颈在于字符串比较,考虑如何优化

发现每次为 \(\text{prf}(res_{i-1},j)[+s_i]\)\(\text{prf}(res_{i-1},k)[+s_i]\) 比较的形式

经过分类讨论,都可以转化为 \(res_{i-1}\)\(s_i\) 的子串 与 \(s_i\) 的前缀的比较

若将 \(s_i\)\(res_{i-1}\) 拼接起来,则两者合并为拼接后字符串的子串和其前缀的比较

这可以通过 \(z\) 函数实现

这样总时间复杂度为 \(O(nk+\sum |s_i|)\),常数极大,空间复杂度应该可以滚动数组优化到 \(O(k+\max s_i)\)

代码

参考

  1. Z 函数(扩展 KMP) - oi wiki
  2. 字符串乱学
posted @ 2025-03-17 19:35  Hstry  阅读(14)  评论(0)    收藏  举报