字符串回炉重造(KMP/Z函数篇)

\(\text{KMP}\)

思想简述

\(\text{KMP}\) 算法本质是用失配指针来优化暴力配对

而我们用最长 \(\text{border}\) 来当做失配指针的本质在于:两个成功匹配区间的交必定是模式串的 \(\text{border}\)

解决的经典问题:假设我们有两个长度分别为 \(n,m\) 的串 \(s,t\)。我们现在要找出 \(t\)\(s\) 中所有出现位置的左端点下标。

算法步骤

从左往右扫,如果每次下一个字符无法配对,就通过跳失配对指针。
因为下一个出现位置和当前的交必定是一个 \(t\)\(\text{border}\)

求解 \(t\)\(\text{border}\) 方法类似,只不过是 \(t\) 的自我匹配过程,利用的是之前已有的信息。

特别的,记 \(t[1,i]\) 的最长 \(\text{border}\)\(nxt_i\)

引理性质总结

引理 \(1\):如果一个 \(s\)\(\text{border}\) \(k\) 的长度不小于 \(s\) 的一半,那么 \(s\) 有一个长为 \(|k|\)\(\text{Period}\)
逆定理同样成立。

引理 \(2\):如果 \(p,q\) 都是周期,那么 \(gcd(p,q)\) 也是周期


\(\text{Z Function}\)

思想简述

\(\text{KMP},\text{Z Function},\text{Manacher}\) 三者的核心思想都是通过一些已知信息来优化暴力得到。

特别的,其中 \(\text{Z}\) 函数和 \(\text{KMP}\) 的相似之处在于它们都是通过先进行一次预处理,然后利用预处理的数组来优化匹配/求 \(\text{LCP}\) 的复杂度。
\(\text{Z}\) 函数和 \(\text{KMP}\) 的相似之处在于操作上。它们都是了维护一个右端点尽量大的区间,然后再求解新值的时候利用一些对等关系求出一个下界,然后再暴力匹配来求解。

解决的经典问题:假设我们有两个长度分别为 \(n,m\) 的串 \(a,b\)。我们现在要对于所有 \(i\),求 \(p_i=\text{LCP}(a[i,n],b[1,m])\)

算法步骤

\(z_i\)\(\text{LCP}(b[1,m],b[i,m])\)

考虑按照 \(i\) 从小到大求 \(z_i\) 的过程。维护一个右端点尽量大的区间 \([l,r]\),这个区间由前面的所有 \([j,j+z_j-1]\) 更新而来。
每次维护出 \([i,i+z_i-1]\)\([l,r]\) 的交部分,然后其它部分暴力扩展即可。

对于 \(p_i\) 的维护同理,唯一的区别在于维护 \([i,i+p_i-1]\)\([l,r]\) 的交部分的时候要借用 \(z\) 数组的信息完成。

两次处理,每次复杂度皆为 \(r\) 每次增大的量之和,即为 \(O(n)\)


一些练习

UVA11452 求最小循环节

直接用引理 \(1\) 做即可。

BZOJ4974\(nxt\) 构造最小字典序的串

这题考对 \(\text{KMP}\) 中求解 \(nxt\) 数组整个过程的理解。

实际上就是逐位确定答案——按照求 \(nxt\) 的过程模拟,模拟的过程中记下当前位置不能放什么字符,最后贪心取最小可行字符即可。

CF1137B

考察对于 \(\text{KMP}\) 失配指针的意义与使用。

这题一开始有个直观的想法就是一堆 \(t\) 串直接头尾拼接,直到拼不了为止、

但是容易发现,可能会有更优情况存在:一个 \(t\) 的后缀可以作为下一个 \(t\) 的前缀重复利用。
而重负利用 \(\text{border}\) 部分就是 \(\text{KMP}\) 失配指针做的事。直接处理出 \(nxt\) 数组并用其减去重复利用部分的贡献即可。

UVA12467

这题考如何将题目转化成字符串匹配模板,不考 \(\text{KMP}\) 原理。

为了方便,我们将题意转化为:找一个 \(S\) 的前缀,使得它在 \(S\) 的反串中出现过。

\(T\)\(S\) 的反串。现在我们只需要求,\(S\) 的所有在 \(T\) 里出现过的前缀中,拥有最大长度的那一个。
这东西显然一遍字符串匹配可以解决。

CF471D

这题也是考建模。

所谓“形状相同”,实则是所有相邻两个位置高度差相同。
故而考虑做差分数组后字符串匹配。最终答案即为匹配上的个数。

记得特判 \(w=1\) 的情况,此时的答案为 \(n\)

HDU3336 求所有前缀出现次数

有两种计数方式:

  1. 在出现的位置进行计数
  2. 对于每个前缀,求出现次数的总和
  • 先说第一种做法。
    对于前缀 \(s[1,j]\),注意到如果存在一个出现位置 \(i\) 满足 \(s[1,j]=s[i-j+1,i]\),那么这个串会对答案产生 \(1\) 的贡献。
    此时容易发现,因为 \(s[1,nxt_i]=s[i-nxt_i+1,i]\),所以应当有 \(nxt_i\) 的贡献。
    但实则不然,我们在 \(nxt_{i-1}+1=nxt_i\) 的情况下会产生重复计数,而重复计数的量就是 \(nxt_{i-1}\)。考虑减去重复的数量即可。

  • 再讲第二种更直观一点的做法。
    根据判定条件 \(s[1,j]=s[i-j+1,i]\),容易发现一个前缀在串中的出现次数就是这个串作为其它前缀 \(\text{border}\) 的次数。
    而根据 \(\text{border}\) 理论,\(\text{border}\)\(\text{border}\)\(\text{border}\)
    故而考虑维护一个数组 \(f\) 表示出现次数。每次直接令 \(f_{nxt_i}\leftarrow f_i\) 便可。

第二种做法也可以用 \(\text{fail}\) 树的角度理解。

CF119D

本题考察对于 \(\text{KMP}\)\(\text{Z Function}\) 的运用熟练度。

这题可以直接按照题目做,也可以转化为 \(R(f(a,i,j))=R(b)\) 去做。
这里就提一下不转化怎么做。

我们尝试枚举 \(i\)

  • 先考虑 \(a[0,i]\) 的部分,这东西要和 \(r(b)\) 的前缀匹配。可以直接暴力一位位叛过去。
  • 再考虑 \(a[i+1,j-1]\) 的部分,这东西要和 \(b\) 的一个前缀匹配,可以用 \(\text{Z function}\) 来做。
  • 最后考虑 \(a[j,n-1]\) 的部分,这部分中抛开边界情况不谈,就是要求 \(b\) 的某个前缀和 \(a\) 反串的前缀的最大匹配长度。可以用 \(\text{KMP}\) 解决。

最后最小的 \(j\) 取可行区间的下界即可。

具体细节可以见这篇文章

CF1968G2

调和级数+\(\text{Z Function}\) 题。

显然最外面从小到大枚举 \(\text{LCP}\) 长度,求最大划分段数。然后更新一下答案就行。

现在问题在于怎么求最大划分段数。

\(z_i\)\(|\text{LCP}(w_1,w_i)|\)
考虑先写一下划分合法的条件:

  1. \(\forall 1\leq i \leq k,z_i\geq l\)
  2. 划分出串互不相交

注意,由于这里有不相交的良好性质,所以对于所有 \(l\),划分串的总数 \(\leq n\log n\)
这是一个调和级数。

于是现在我们就要在较低的时间复杂度内找到下一个 $z_i\geq l 的位置。
这东西可以用并查集维护,考虑把所有 \(z_i<l\) 的并成一个块,同时维护块内最大编号节点。
这样每次要找的位置就是——块内最大编号节点 \(+1\)

时间复杂度 \(O(n\log^2 n)\)

posted @ 2025-02-24 20:51  徐子洋  阅读(6)  评论(0编辑  收藏  举报