「笔记」后缀树

写在前面

《关于我把后缀树题数据范围缩小出成模拟赛题让 63 级做并说标算只是一个简单的 Trie 树这件事是否可行》

个人认为后缀树是一种很鸡肋的数据结构。
它能做的 SAM 都能做,构建后缀树都可以直接用 SAM = =
但是简单了解后缀树可以加深对 SA 和 SAM 的理解,这玩意又很简单,我建议简单了解即可。

定义

对于一个字符串 \(S\),它的后缀树是由其所有后缀 \(S[i:n]\,(1\le i\le n)\) 组成的,经过信息压缩后的 Trie 树。

暴力构建

考虑增量法,暴力枚举原串的每个后缀,将其插入字典树。
本质不同的子串个数最多有 \(O\left(\frac{n^2}{2}\right)\) 个,故节点数最多可能达到 \(O(n^2)\) 级别。
此时使用后缀树与直接枚举原串的子串等价,复杂度为 \(O(n^2)\) 级别。

Ukkonen

可以在线性时间复杂度内构建后缀树。

构建后缀树的常用算法,推荐阅读这篇博文进行学习:炫酷后缀树魔术 _ EternalAlexander的博客

虚树 + SA

虽然节点数可能达到 \(O(n^2)\) 级别,但只有 \(n\) 个后缀,终止状态数仅有 \(n\) 个。
大部分节点仅有一个孩子,这样的链信息可以合并。考虑建后缀树的虚树,缩链成边。

前置知识

SA + 虚树

简单介绍虚树(抄一波课件):

对于树 \(T=(V,E)\),给定关键点集 \(S\subseteq V\),则可定义虚树 \(T'=(V',E')\)
对于点集 \(V'\subseteq V\),使得 \(u\in V'\) 当且仅当 \(u\in S\),或 \(\exist x,y\in S,\operatorname{lca}(x,y)=u\)
对于边集,\((u,v)\in E'\),当且仅当 \(u,v\in V'\),且 \(u\)\(v\)\(V'\) 中深度最浅的祖先。

个人理解:
仅保留关键点及其 lca,缩链成边。
可能删去一些不包含关键点的子树。压缩了树的信息,同时丢失了部分树的信息。
显然,一棵后缀树的关键点 即其 \(n\) 个终止状态。

一个分叉点会合并至少两个点,当后缀树为完全二叉树时虚树节点数最多,为 \(2n-1\) 个,节点数变为了 \(O(n)\) 级别。

构建方法

假设已知后缀树的结构
考虑增量法,每次向虚树中添加一个关键点。

先求得 关键节点 的 dfs 序,规定按字典序 dfs,按照 dfs 序添加关键节点。
单调栈维护虚树最右侧的链(上一个关键点与根的链),单调栈中节点深度递增,栈顶一定为上一个关键点。
每加入一个关键点 \(a_i\),令 \(\operatorname{lca}(a_{i-1},a_i)=w\)
将栈顶 \(dep_x > dep_w\) 的弹栈,加入 \(w,a_i\),即为新的右链。特别地,若栈顶存在 \(dep_x=dep_w\),则不加入 \(w\) 节点。

在此过程中维护每个节点的父节点,在弹栈时进行连边并维护信息,即得虚树。

套 SA

但是后缀树的结构并不已知, 已知还建虚树干什么 发现上述过程中并没有用到后缀的性质。
可以总结一下上述建虚树的过程:

  • 求关键点的 dfs 序。
  • 单调栈维护右链。
  • 插入关键节点,求两相邻关键点的 \(\operatorname{lca}\),比较深度。

对于前两步,关键点 \(i\) 的 dfs 序即为后缀数组中的 \(sa_i\),可使用 DC3 算法 \(O(n)\) 求得。单调栈的复杂度为 \(O(n)\)

发现两关键节点 代表排名相邻的 两后缀。插入 \(sa_i\) 时,\(1\sim \operatorname{lca}\) 的链代表两后缀的最长公共前缀,即 \(\operatorname{lcp}(sa_{i-1}, sa_i)\)
显然 \(\operatorname{lcp}\) 的长度,同时也是 \(\operatorname{lca}\) 的深度,等于 \(\operatorname{height}_i\)

\(\operatorname{lca}\) 节点是谁不知道,但这并不妨碍弹栈,弹栈过程只关心节点的深度。
弹栈停止后,若栈顶存在 \(dep_x=\operatorname{lcp}\),则 \(\operatorname{lca}\) 已在栈中,直接停止弹栈。否则新建一个 \(dep_x=\operatorname{lcp}\) 的节点插入,当做 \(\operatorname{lca}\) 即可。

使用倍增实现 SA,复杂度 \(O(n\log n)\)。使用 DC3 算法复杂度为 \(O(n)\) 级别。

后缀自动机

SAM 的 parent 树是反串的后缀树。
建出反串的 SAM 之后,可以直接得到一棵后缀树。
时间复杂度为 \(O\left(n\left| \sum\right|\right)\)\(O(n\log n)\)

与 SA 的关系

SA 可以看作由后缀树的所有终止状态按字典序排列得到的数组。
SA 的 \(\operatorname{height}\) 数组可以看做后缀树中相邻两终止状态的 \(\operatorname{lca}\) 的深度,从这个角度上可以更加形象直观地理解 SA 的许多性质。

同时,这也证明了后缀树的适用范围一定比 SA 大。
奈何 SA 配合 \(\operatorname{height}\) 可以实现大部分后缀树的功能,又好写,时间上与后缀树又几乎相同,空间又小,所以我喜欢 SA(

做题的时候可以先直观地从后缀树的角度出发思考,再用 SA 进行实现。
可以避免抽象的思考过程,比较适合我这样的无脑选手(

例题

这里简述使用后缀树求解的思路,以及如何用后缀树和 SA 进行实现。

SP705 SUBST1 - New Distinct Substrings

\(T\) 组数据,每次给定一个字符串,求该字符串本质不同的子串数量。
两个子串本质不同,当且仅当两个子串长度不等,或长度相等但有任意一位不同。
\(1\le T\le 1\le|s|\le 5\times 10^4\)
280ms,1.46GB。

一种显然的做法是建出后缀树,答案即未压缩信息的后缀树中的节点个数。
在缩链成边的同时维护边中包含的节点个数即可。
总复杂度 \(O(n)\)


另一种想法是用所有子串的个数 \(\frac{n(n+1)}{2}\) 减去重复子串的个数,显然重复的串一定出现在某两个后缀的公共前缀部分。
考虑后缀树上重复统计部分的位置,有下图所示:

ugly

观察其中的单色区域的形态,可以考虑增量法统计答案,按照字典序依次将所有后缀加入到后缀树中。
考虑加入 \(sa_i\) 后,新增的本质不同的子串的数量,显然即 \(\operatorname{dep}(sa_i) - \operatorname{dep}(\operatorname{lca}(sa_i, sa_{i-1}))\),代表不作为之前加入的,\(sa_i\) 的前缀的数量。
字典序相邻的节点的 \(lca\) 的深度即为 SA 中的 \(\operatorname{height}\),则最终答案即:

\[\frac{n(n+1)}{2} - \sum_{i = 2}^{n}\operatorname{height}_i \]

SA 简单实现即可,总复杂度 \(O(n)\sim O(n\log n)\),依赖于实现。

「AHOI2013」 差异

给定一长度为 \(n\) 的字符串 \(S\),令 \(T_i\) 表示从第 \(i\) 个字符开始的后缀,求:

\[\sum_{1\le i<j\le n}\{\operatorname{len}(T_i) +\operatorname{len}(T_j) - 2\times \operatorname{lcp} (T_i,T_j)\} \]

\(\operatorname{len}(a)\) 表示字符串 \(a\) 的长度,\(\operatorname{lcp}(a,b)\) 表示字符串 \(a,b\) 的最长公共前缀长度。
\(1\le n\le 5\times 10^5\)
1S,512MB。

这个式子玩意长得就很树上差分。
对于 \(S\) 的后缀树,\(\operatorname{lcp}\) 即为后缀树的 \(\operatorname{lca}\),则上式等价于后缀树上所有后缀之间的距离之和。
则树上某一点的对答案贡献,即它的 \(\operatorname{dep}\) 乘上以它为 \(\operatorname{lca}\) 的后缀节点的数量。记录子树大小,DP 实现即可。

总复杂度 \(O(n)\) 级别。

写在最后

参考资料:

OI-wiki 虚树
利用后缀数组构造后缀树_AZUI
IOI2004 国家集训队论文 后缀数组 许智磊
炫酷后缀树魔术 _ EternalAlexander的博客

posted @ 2021-01-02 15:04  Luckyblock  阅读(1011)  评论(3编辑  收藏  举报