Loading

前缀函数与Z函数介绍

字符串算法果然玄学=_=
参考资料:
OI Wiki:前缀函数与KMP算法
OI Wiki:Z函数(扩展KMP)

0. 约定

字符串的下标从 \(0\) 开始。\(|s|\) 表示字符串 \(s\) 的长度。
对于字符串 \(s\),记其每一个字符分别为 \(s_0, s_1, \cdots, s_{|s|-1}\)
子串 \(s_l, s_{l+1}, \cdots, s_{r-1}, s_r\) 简记为 \(s[l:r]\)。特别地,若 \(l=0\),可记作 \(s[:r]\);若 \(r=|s|-1\),可记作 \(s[l:]\)
对于字符串 \(a, b\)\(a+b\) 表示拼接操作,即将字符串 \(b\) 拼接到字符串 \(a\) 之后,构成新的字符串。
记构成的新字符串为 \(c\),则上述拼接操作记为 \(c\gets a+b\)
其中符号 \(x\gets y\) 表示将 \(y\) 的值赋给 \(x\)
不论是字符还是字符串,皆不加引号。

1. 前缀函数

1.1. 前缀函数的定义

对于字符串 \(s\),若存在一个非本身的子串 \(t\) 使得 \(t\) 既是 \(s\) 的前缀又是 \(s\) 的后缀,称 \(t\)\(s\) 的一个 \(\text{Border}\)
更加符号化地,对于一个长为 \(n\) 的字符串 \(s\),若存在 \(m\) 使得 \(m\neq n\)\(s[:m-1]=s[n-m:]\)
\(s[:m-1]\) (\(s[n-m:]\) 同理) 为 \(s\) 的一个长度为 \(m\)\(\text{Border}\)
对于一个字符串 \(s\),定义 \(\text{MaxBorder}(s)\)\(s\)\(\text{Border}\) 中最长的。
接下来定义前缀函数:
对于一个字符串 \(s\),定义前缀函数 \(\pi(s,i)=\left|\text{MaxBorder}(s[:i])\right|\)
在不引起歧义的情况下,可简记为 \(\pi(i)\);特别地,定义 \(\pi(s,0)=0\)
有时我们关心一整个字符串的前缀函数值,故此时我们也可以记 \(\pi(s)=\{\pi(s,0), \pi(s,1), \cdots, \pi(s,|s|-1)\}\)

举一个实例:\(\pi(\texttt{abacaba})=\{0,0,1,0,1,2,3\}\)

1.2. 前缀函数的重要性质

显然直接根据定义计算前缀函数的时间复杂度是不能接受的,于是我们需要依赖一些有用的性质来加速计算。
主要的性质有下面两个:

  1. \(\pi(i+1)\leqslant\pi(i)+1\),且仅当 \(s_{\pi(i)}=s_{i+1}\) 时有 \(\pi(i+1)=\pi(i)+1\)
  2. \(j\)\(s[:i]\) 次长的 \(\text{Border}\) 的长度,则 \(j=\pi(\pi(i)-1)\)

第一个性质是显然的。第二个性质由 \(s[:j-1]=s[i-j+1:i]=s[\pi(i)-j:\pi(i)-1]\) 自然导出。

1.3. 快速求前缀函数

这样我们就有了快速求前缀函数的算法:
我们从前向后迭代求。假设我们已经求出了 \(\pi(0), \pi(1), \cdots, \pi(i-1)\),现在要求 \(\pi(i)\)
\(j=\pi(i-1)\)。如果 \(s_i=s_j\),则 \(\pi(i)=j+1\)
否则,令 \(j\gets \pi(j-1)\),重复以上判断直到满足 \(s_i=s_j\)\(j=0\)
\(j=0\),则 \(\pi(i)=0\)

如此重复直到前缀函数全部计算完成。可以证明,时间复杂度为 \(O(n)\)

code:

void prefix()
{
    for(int i=1;i<=n-1;i++)
    {
        int j=pi[i-1];
        while(j>0&&s[i]!=s[j])j=pi[j-1];
        if(s[i]==s[j])j++;
        pi[i]=j;
    }
}

1.4. 前缀函数的应用

1.4.1. KMP

luoguP3375
一般的方法是通过 \(\pi(i)\) 优化匹配,这里就不介绍了。
我们有个简单粗暴地多的方法。
假设模式串为 \(s\),文本串为 \(t\)\(s\) 的长度为 \(n\)\(t\) 的长度为 \(m\)
构造字符串 \(a\gets s+\#+t\),其中 \(\#\)\(s,t\) 中均不会出现的字符。
然后求 \(a\) 的前缀函数,若\(\pi(a,i)=n\),则 \(s\)\(t\)\(i-2n\) 处出现。
总时间复杂度 \(O(n+m)\)。代码略。
优化:注意到前缀函数是可以一个一个字符在线处理的,所以空间复杂度可优化到 \(O(n)\)

1.4.2 字符串的周期

luoguP4391
对于字符串 \(s\),若 \(\exist p,\ 0<p\leqslant |s|\) 使 \(\forall i\in [0,|s|-p-1],\ s_i=s_{i+p}\),则称 \(p\)\(s\) 的周期。
运用 \(\text{Border}\) 理论,可以发现,若 \(s\) 存在一个长为 \(l\)\(\text{Border}\),则 \(|s|-l\)\(s\) 的周期。
我们应用之前的结果,可知 \(|s|-\pi(|s|-1),\ |s|-\pi(\pi(|s|-1)-1),\cdots\)\(s\) 的周期,其中最小的周期为 \(|s|-\pi(|s|-1)\)

1.4.3 统计每个前缀的出现次数

根据前缀函数的定义及其性质,以 \(i\) 为右端点有长度为 \(\pi(i),\ \pi(\pi(i)-1),\cdots\) 的前缀。
我们不能按照端点来统计,因为在极端情况下,如 \(\texttt{aaaaaa}\),时间复杂度达到了平方级别。
但是注意到每一个长为 \(i\) 的前缀的出现中也一定包含长为 \(\pi(i-1)\) 的前缀的出现,因此我们可以考虑用较长的前缀的值来更新较短的前缀的值。(具体见代码)
最后别忘了加上每个前缀在初始位置出现的一次。
code:

for(int i=1;i<n;i++)ans[pi[i]]++;
for(int i=n-1;i>0;i--)ans[pi[i-1]]+=ans[i];
for(int i=1;i<=n;i++)ans[i]++;

2. Z函数

2.1. Z函数的定义

定义 \(\text{lcp}(a,b)\) 为字符串 \(a, b\) 的最长公共前缀。
定义Z函数:
对于字符串 \(s\),定义Z函数 \(z(s,i)\)\(\text{lcp}(s,s[i:])\) 的长度。
在不引起歧义的情况下,可简记为 \(z(i)\);特别地,定义 \(z(s,0)=0\)
根据以上定义,我们有 \(s[:z(i)-1]=s[i:i+z(i)-1]\);对此我们称 \(\text{Z-box}(i)=s[i:i+z(i)-1]\)
有时我们关心一整个字符串的Z函数值,故此时我们也可以记 \(z(s)=\{z(s,0), z(s,1), \cdots, z(s,|s|-1)\}\)

举一个实例:\(z(\texttt{abacaba})=\{0,0,1,0,3,0,1\}\)

2.2. 快速求Z函数

想要快速求出Z函数的值,我们需要充分发掘Z函数的性质以使信息得到高效利用。
考虑若\(l \leqslant i \leqslant l+z(l)-1\),即 \(i\)\(\text{Z-box}(l)\)之内:
方便起见,设 \(r=l+z(l)-1\)
根据Z函数的定义有 \(s[i-l:r-l]=s[i:r]\),则 \(s[i-l:i-l+z(i)-1]=\text{Z-box}(i)=s[:z(i)-1]\)
很明显,若 \(z(i-l)<r-i+1\),则直接有 \(z(i)=z(i-l)\);否则,我们能得到 \(z(i) \geqslant r-i+1\),想要得出具体数值还需向后枚举。

为了更好地利用上述性质,我们可以在计算过程中维护最大的 \(r\)。具体操作如下:
初始时令 \(l=r=0\)
从前到后开始计算。若 \(i\leqslant r\),利用上述性质进行计算。否则,暴力枚举。
如果新的 \(r\) 比之前的更大,即 \(i+z(i)-1>r\),更新 \(l=i,\ r=i+z(i)-1\)
code:

z[0]=l=r=0;
for(int i=1;i<n;i++)
 {
    if(r>i)z[i]=min(z[i-l],r-i+1);//简便起见,我们可以直接让z[i]取为z[i-l]和r-i+1中较小的一个
    for(;b[i+z[i]]==b[z[i]];z[i]++);//暴力枚举
    if(i+z[i]-1>r){l=i;r=i+z[i]-1;}//更新l,r
}

可以看到每个字符只会被暴力匹配一次,因此复杂度为 \(O(n)\)

这个网站有对该算法的演示。
luoguP5410
对于这道题,我们还要求 \(b\)\(a\) 的每一个后缀的 \(\text{lcp}\)。可以对之前的算法进行修改,也可以直接把 \(a\) 接到 \(b\) 的后面来做。

posted @ 2021-07-14 20:11  pjykk  阅读(541)  评论(0编辑  收藏  举报