后缀数组入门

由于后缀数组大多数可以被 后缀树 / 后缀自动机 所替代,因此这里涉及到的后缀数组的知识较浅。

定义

字符串 \(S\),定义其后缀数组 \(sa\) 为将所有后缀按照字典序排序后的开头下标构成的数组。

相应地,定义 \(rk_i\) 为后缀 \(S_{i, n}\) 在后缀数组当中的排名。

求解

同样考虑使用增量法。

但由于要求出所有后缀按照字典序排序后的数组,我们考虑对每一个后缀都进行增量。

具体地,假设当前已经获得了所有后缀前 \(i\) 个字符构成的后缀子串的后缀数组,记作 \(sa_{i, 1 \sim n}\),要得到 \(sa_{i + 1, 1 \sim n}\)

那么我们可以按照 \(sa_{i, 1 \sim n}\) 为第一关键字,每个后缀的第 \(i + 1\) 个位置上的字符为第二关键字进行排序。

可以发现这样复杂度是 \(\mathcal{O(n ^ 2\log n)}\) 的,并不优秀。

但可以发现每次我们可以不只增加一个字符进去比较,而是可以再增加长度为 \(i\) 的字符比较。

也就是说,我们将每个后缀按照 \(sa_{i, j}\) 为第一关键字比较,\(sa_{i, j + i}\) 为第二关键字比较进行排序。

不难发现这样是倍增进行计算的,复杂度降至 \(\mathcal{O(n \log ^ 2n)}\)

事实上,我们发现 \(sa\) 数组的值域是 \(\mathcal{O(n)}\) 的,于是可以直接进行基数排序,复杂度又可降至 \(\mathcal{O(n \log n)}\)

基础应用

寻找最小的循环移动位置

例题:「JSOI2007」字符加密

直接将原串复制一遍接到原串的后面,问题即变为后缀排序问题。

在字符串中找子串

  • 开始给定文本串,在线输入所有模式串,求模式串在文本串当中的出现次数,保证所有字符串串长 \(\le 5 \times 10 ^ 5\)

对文本串建出后缀数组,文本串如果出现一定为某个后缀的一个前缀。

因此在后缀数组 \(sa\) 中,文本串出现的位置一定是一段区间。

我们二分得到这个区间的左右端点,\(check\) 直接暴力求两个串的 \(\rm LCP\) 即可。

复杂度 \(\mathcal{O}(|S| \log |S| + \sum |T| \log |S|)\)

从字符串首尾取字符最小化字典序

例题:「USACO07DEC」Best Cow Line

考虑模拟这个流程,假设当前选取到了区间 \([l, r]\)

那么我们比较 \(S_{l, r}\) 及其反串字典序,若前者大则移 \(l\) 否则移 \(r\),这样显然不劣。

于是问题就变为一个子串及其反串的字典序比较问题,我们将其放缩为 \(S_{l, n}\) 这个后缀和 \(S_{1, r}\) 这个前缀的反串的字典序比较问题。

这样就非常简单了,我们将原串的反串加到原串后面,于是就变为了后缀排序问题。

复杂度 \(\mathcal{O(n \log n)}\)

height 数组

定义

我们定义 \(height_i = \mathrm{LCP}(sa_{i - 1}, sa_i)\),特别地:\(height_1 = 0\)

求解

我们给出如下事实:

  • \(height_{rk_i} \ge height_{rk_{i - 1}} - 1\)

反证法。
若不成立则 \(sa_{rk_{i - 1} - 1}\)\(sa_{rk_i - 1}\) 更靠近 \(sa_{rk_i}\),注意一些细节即可。

由此,我们直接按照 \(1 \sim n\) 的顺序暴力依次求出 \(height_{rk_i}\),每次的初始答案置为下界即可。

容易得知这样的复杂度为 \(\mathcal{O(n)}\)

应用

两个后缀的最长公共前缀

同样有如下事实:

  • \(\mathrm{LCP}(sa_i, sa_j) = \min\limits_{k = i + 1} ^ j height_k\)

我们考虑将所有的后缀插入到一颗 \(\tt trie\) 树当中。
那么两个后缀的 \(\rm LCP\) 即为其在 \(\tt trie\) 树上的终止节点的 \(\rm LCA\) 的深度。
考虑确定 \(\rm LCA\) 的点对为 \(x\) 的方法,实际上后缀排序就相当于在 \(\tt trie\) 树上的 \(dfs\) 序,两者就建立起了练习。

由此,我们可以使用 \(\rm ST\) 表优化到 \(\mathcal{O}(n \log n)\) 预处理,\(\mathcal{O(1)}\) 查询。

比较一个字符串的两个子串的大小关系

假设两个子串分别为 \([l_1, r_1], [l_2, r_2]\)

首先求后缀 \(S_{l_1, n}, S_{l_2, n}\)\(\mathrm{LCP} = x\),若 \(x\) 超过了两者串长的较小值,那么按照长度为关键字比较,否则比较 \(x\) 的下一位即可。

不同子串的数目

考虑容斥,计算重复出现的子串数目之和。

我们按照子串出现的时间顺序定义子串是否重复,更进一步地,我们将后缀排序后的第 \(i\) 个后缀的前缀出现时间设为 \(i\)

那么第 \(i\) 个后缀重复出现的前缀个数为 \(height_i\),因此重复的子串数目之和为:

\[\sum\limits_{i = 1} ^ n height_i \]

出现至少 k 次的子串的最大长度

例题:「USACO06DEC」Milk Patterns

假设字符串 \(T\)\(S\) 中出现了,那么出现的位置必定为若干个后缀的前缀。

因此我们可以考虑 \(T\)\(S\) 的所有后缀当中是否出现,进一步地,我们发现其一定出现在后缀数组的一个区间。

又我们只关心答案的最大长度,因此不在乎具体为哪个子串。

那么如果存在一个长度为 \(x\) 的子串出现了 \(k\) 次,那么后缀数组上一定存在一个长度为 \(k\) 的区间使得区间内任意两个后缀的 \(\rm LCP\) 长度均不小于 \(x\)

换句话说,即存在一个长度为 \(k - 1\) 的区间使得该区间内的 \(height\) 数组的最小值 \(\ge x\)

又如果一个长为 \(x\) 子串出现了至少 \(k\) 次,那么一定存在长为 \(1 \sim x - 1\) 的子串出现了至少 \(k\) 次。

因此原问题转化为求 \(height\) 数组中所有长度为 \(k - 1\) 的区间的最小值的最大值。

不难发现这就是滑动窗口,可以直接做了。

询问最长的子串使得其在文本串中至少不重叠地出现了两次

二分答案 \(mid\)

我们枚举其在第二次出现的开头位置 \(x\),相当于询问是否存在一个位置 \(y\) 满足 \(y \le x - mid\) 使得 \(S_{x, n}, S_{y, n}\)\(\rm LCP\) 长度不小于 \(mid\)

我们先限制与 \(x, \rm LCP\) 长不小于 \(mid\) 的字符串在后缀数组上对应的区间 \([l, r]\)

问题转化为询问 \([l, r]\) 内是否存在一个点使得其下标不大于 \(x - mid\),不难发现直接维护区间最小值即可。

使用 \(\rm ST\) 表可做到 \(\mathcal{O(n \log n)}\)

结合并查集

这类问题通常要求在两个后缀的 \(\rm LCP\) 大于某个值的情况下求若干信息。

因此可以按照 \(height\) 数组进行排序,将不小于某个值的 \(height\) 连接的相邻两个节点连边,那么此时构成的连通块内任意两个后缀之间的 \(\rm LCP\) 均不小于 \(x\)

结合线段树

具体问题具体分析。

结合单调栈

类似「结合并查集」,通过单调栈求出每个 \(height\) 管辖的最小值区间,从而将所有后缀点对按照 \(\rm LCP\) 长度划分成 \(n\) 个区间,这样就可以来求解题目要求的信息了。

posted @ 2021-06-13 16:51  Achtoria  阅读(46)  评论(0编辑  收藏  举报