前缀函数

前缀函数通常称为 border,一个字符串 SS 的 border 定义为它的一个前缀子串 t(tS)t(t \ne S),满足 tt 既是 SS 的前缀,也是 SS 的后缀。下文的 border 均为 SS 的最长 border 长度。

简单来说,对于一个字符串 S=abcabcdS = \texttt{abcabcd}(下标从 11 开始),它的前缀函数为 [0,1,0,1,2,2,3][0, 1, 0, 1, 2, 2, 3]

计算方法

单纯暴力

从左向右遍历一遍,每次循环一次长度,每次检查遍历一遍子串,复杂度为 O(n3)O(n^3)

边界优化

从左向右遍历的过程中,子串的大小每次只会增加 11,而原先的前缀不会改变,因此 borderiborder_i 的长度最多只会比 borderi1border_{i - 1}11,在这种情况下:

S[i]=S[borderi1+1]S[1borderi1]=S[i1borderi1+1i1]S[1borderi1+1]=S[i1borderi1+1i]borderi=borderi1+1S[i] = S[border_{i - 1} + 1] \Rightarrow S[1 \dots border_{i - 1}] = S[i - 1 - border_{i - 1} + 1 \dots i - 1] \\ \Rightarrow S[1 \dots border_{i - 1} + 1] = S[i - 1 - border_{i - 1} + 1 \dots i] \\ \Rightarrow border_i = border_{i - 1} + 1

可见这种优化下,字符串比较次数与 border 有关,border 越长,字符串越长,而比较次数越短,根据势能分析可知,在这种情况下,字符串比较的时间复杂度为 O(n)O(n),总共的时间复杂度为 O(n2)O(n^2)

遍历优化

不难发现,每一次失配之后,我们都会将 borderiborder_i11,从而造成了大量的浪费,考虑从这方面入手。

目标就是找到一个下标 jj,满足 S[1j]=S[ij+1i]S[1 \dots j] = S[i - j + 1 \dots i],我们可以惊奇的发现,在之前的子串 S[1borderi1]S[1 \dots border_{i - 1}] 之中也存在一个 k=borderborderi1k = border_{border_{i - 1}},满足:

S[1k]=S[i1k+1i1]S[1 \dots k] = S[i - 1 - k + 1 \dots i - 1]

这个 kk 值一定是除了 borderi1border_{i - 1} 之外最大的 jj,因为他本身是子串 S[1borderi1]S[1 \dots border_{i - 1}] 的最大的 border,到这里操作就十分简单了,每次失配后让 jborderjj \leftarrow border_{j} ,直到 S[i]=S[j+1]S[i] = S[j + 1] 或者 j=0j = 0 即可。

实现代码

1
2
3
4
5
6
for (int i = 2, j = 0; i <= m; i ++)    // i == 1 时 t 不是 s 的真子串
{
while (j && b[i] != b[j + 1]) j = nxt[j];
if (b[i] == b[j + 1]) j ++;
nxt[i] = j;
}

KMP 算法

P3375 【模板】KMP - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

KMP 算法是前缀函数的典型应用。

算法思路

我们定义 SS 为文本串,TT 为模式串,要求找到 SS 中所有 TT 的位置。

这个问题可以简单地进行转化:定义 fif_i 表示满足 T[1fi]=S[ifi+1i]T[1 \dots f_i] = S[i - f_i + 1 \dots i] 的最大值,不难发现,如果 fi=Tf_i = |T|,那么 SS 中的位置 iT+1i - |T| + 1 便是 TT 的一个位置。

思路十分简单,先求出 TT 的前缀函数,在用 TT 的前缀函数 nxt[]nxt[] 用来更新 fif_i

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// a:文本串 b:模式串 n:|a| m:|b| 下标为 1
for (int i = 2, j = 0; i <= m; i ++)
{
while (j && b[i] != b[j + 1]) j = kmp[j];
if (b[i] == b[j + 1]) j ++;
kmp[i] = j;
}

for (int i = 1, j = 0; i <= n; i ++)
{
while (j && a[i] != b[j + 1]) j = kmp[j];
if (a[i] == b[j + 1]) j ++;
if (j == m) cout << i - m + 1 << '\n';
}

Z 函数 / exKMP

P5410 【模板】扩展 KMP/exKMP(Z 函数) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

Z 函数与前缀函数的定义相似,关于字符串 SS 的 Z 函数中 ziz_i 表示满足 S[1zi]=S[ii+zi1]S[1 \dots z_i] = S[i \dots i + z_i - 1] 的最大值。

计算方法

由于不同的性质,Z 函数的求法与前缀函数完全不同,采取的是类似 Manacher 的双指针递推方法。

假设我们处理完了前 i1i - 1 个,令 rr 表示前 i1i - 1 满足 Z 函数定义的最右边界,ll 则是与 rr 相对应的左边界(也就是满足 rr 的下标)。对于第 ii 个子串,有如下几种情况:

  • i>ri \gt r :说明前面的处理与第 ii 个无关,直接暴力遍历即可。
  • iri \le r:根据定义,S[1zl=S[lr]S[1 \dots z_l = S[l \dots r],然而 iri \le r,那么推出 S[il+1zl=S[ir]S[i - l + 1 \dots z_l = S[i \dots r],既然如此,我们让 zimin(zil+1,ri+1)z_i \leftarrow \min (z_{i - l + 1}, r - i + 1),取 min\min 的原因是无法保证 rr 的右边字符串是否仍然相等。

和 Manacher 算法一样,指针 rr 只会向右移动,每次更新也会更新 rr,时间复杂度为 O(n)O(n)

代码实现

1
2
3
4
5
6
7
8
9
10
11
void Z_Function()    // 下标为 1
{
z[1] = m; // i == 1 时子串是其本身
for (int i = 2, l = 0, r = 0; i <= m; i ++)
{
int k = i > r ? 0 : min(z[i - l + 1], r - i + 1); // 分类讨论
while (i + k <= m && b[i + k] == b[k + 1]) k ++;
z[i] = k;
if (i + k - 1 > r) l = i, r = i + k - 1; // 更新 l 和 r
}
}

exKMP

求出字符串 SS 与字符串 TT 中每一个后缀的 LCP(Longest Common Prefix)。

采取与 KMP 算法类似的思想,先预处理出 TT 与其本身的 Z 函数,再用这个 Z 函数更新 SS 串即可。

代码实现

1
2
3
4
5
6
7
8
9
10
11
// a:S b:T n:|S| m:|T|
void exKMP()
{
for (int i = 1, l = 0, r = 0; i <= n ; i ++)
{
int k = i > r ? 0 : min(z[i - l + 1], r - i + 1);
while (i + k <= n && a[i + k] == b[k + 1]) k ++;
p[i] = k, ansp ^= (p[i] + 1) * i;
if (i + k - 1 > r) l = i, r = i + k - 1;
}
}

Reference

前缀函数与 KMP 算法 - OI Wiki (oi-wiki.org)

P3375 【模板】KMP - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

Z 函数(扩展 KMP) - OI Wiki (oi-wiki.org)

P5410 【模板】扩展 KMP/exKMP(Z 函数) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)