后缀自动机学习笔记

后缀自动机

也叫SAM(suffix automaton)

自闭了。我实在是理解不了后缀自动机的建立和一些性质 我真的理解不了。不浪费时间了 等有空了再理解吧。

只要会用就行了 我一定会用的非常的熟练的加油~但是这篇学习笔记还是要有的 如果哪一天突然的顿悟了呢?前进前进。

注意我们学习的东西叫做后缀自动机 特点:

1. 后缀自动机是一张有向无环图

2. 后缀自动机的每个状态表示从初始状态沿不同路径到达该状态时,按顺序记下转移边上的字母得到的字符串的集合。

3. 所有结束状态的并集表示字符串的所有后缀的集合。

4. 所有状态表示字符串都是原字符串的子串。

且点数和边数都为O(n)数量级. 为什么SAM可以体现出一个字符串的所有子串?仔细思考我们把一个串的所有后缀都放到这个后缀自动机上使其可以得到识别 那么显然 一个字符串的所有子串也都是可以得到体现的。

考虑如何构建满足这些要求的后缀自动机。endpos 即right集比较好理解这里不再赘述 重点是在建后缀自动机的过程。

这里简单的提出几个结论或者说是性质:

Right集越大 状态中子串越少。

母串的每个子串必然在SAM的某个状态里。

一个状态的Right集合等于该状态在parent树中所有儿子Right集合的并集.

接下来是具体的构造过程了。

在线 每插入一个字符完成一次状态转移。假设已经建好了前i个字符的SAM,然后考虑插入了S[i+1]后的情况

首先先出现的这个字母的right集一定是新出现的 所以可以说我们应该先创建一个点表示这个最小的right

第二步 连边 我们只是把最大的ritht集做出来了 还有其他子串还没有被体现出来,那么显然我们应该不断向上跳父亲然后进行构造这些子串的出现。

这一步 right集始终都围绕在我们新建的那个节点上 接下来是扩充right 对于当不断在爬父亲的过程中 我们可以发现随着子串的越来越小 可能right集的等价类刚好包含我们新建节点的等价类。

那么显然 我们应该在parent树上进行连边 扩充当前节点的right集合,那么显然接下来的子串我们不需要继续向下爬了 因为上面已经出现了一个父亲节点接受了所有的状态转移。

那么显然 这样是会出现一些小问题的 我们发现在扩充right集合的时候 一些不合法的情况出现了 一个节点在上一层同时接受多个点的状态转移 且他们的right在上一层中是相同的 但在这一层中是不相同的 所以此时我们需要去分裂节点来维持这一现状显然我们目前的这一节点的right集合是大约现在这个q点的 我们不妨设新建节点为nq 那么显然在parent树的分裂上 nq 同时是q和p的两个节点的right的分裂 我们把q的right集合分离 此时 nq的长度是多少 显然是我们发出q这个节点的上一个节点的 len+1

这才是最长的长度,然后再修改一下之前的关系即可。时间复杂度我还没有能力证明 但是大体过程已经可以叙述完整了.

考虑一下我们的nq的真正含义 表示的是一个right集合 且其是较小的 一堆字母的集合。为什么求本质不同的子串不需要加上其代价?

我想的是 我们因为nq这个节点只是被分裂出来的 为了保证right集合的合法性 至于其存在的数量早在q被建出来时就被累加过代价了。

我们考虑一下 SAM 的模板题目 求一个字符串S所有出现次数不为1的子串的出现次数乘上该子串长度的最大值.

考虑一下right集合相等的子串 显然 他们endpos是相等的 那么说明了他们的出现次数相等 所以我们只需要保留 一个right集合中长度最长的子串即可.

继续考虑 一下一个子串到底出现了 多少次 我们发现 一个right集合中的所有位置都意味着某个子串是出现过的 我们只需要知道当前right集合的大小即可.

显然我们刚开始只是维护了right集合的正确性并不知道其大小 那么我们直接dfs一下parent树就可以知道 某个点的right集合有多大了.

但是存在一个小问题 就是说我们的right集合的大小是不包含那些中转节点的 只包含最后的 单个的endpos节点 那些分裂的出来的节点是没有任何的效益的 其right集合只是表示了一个集合 并不能代表出现的位置,也就是真正有权值的节点只是那些真正的表示位置的节点 剩下的节点只表示一个集合的意思 于是初始化只跟刚开始新建的节点权值初始化为1 分裂节点是没有初值的。

感觉说的还是不够详细 可能还是理解不到位?我真的尽力了。

分裂节点显然应该没有初值 因为考虑一下分裂节点的真实含义 其实是主要意思是把原来的right集合保留下来然后另一个right集合即q所在的right集合缩小,关于一个子串在整个字符串中出现了多少次 这是right集合大小的问题 我们新建节点显然没有扩大right集合 也没有生成新的right集合所以应该是没有权值的。

那我先从应用篇的角度讲述吧:/cy

首先我们构造出来的这个自动机的结构上来说吧 从源点到每一个节点都是一个子串 从源点到终止节点都是我们的原字符串的后缀。

功能1 判断子串 直接拿一个字符串在后缀自动机上跑即可. 可以想AC自动机一样找到在文本串中出现的最大前缀长度.

功能2 S的不同子串的个数 对S建立后缀自动机 每个S的子串都相当于自动机的一些路径这是显然的 且相同的子串的路径不会重复出现 right集合的缘故 所以这个问题等价于以源点为起点的不同路径的条数。 显然我们直接dfs跑是不正确的 因为存在一些小问题 可能方案数更大 但是此时方案数小了 所以从源点开始统计方案数是不对的。但是作为一张有向无环图最显著的特点就是拓扑排序了我们倒着扫 反向建图进行拓扑排序即可.设$f[i]$表示从i出发的子串个数 那么 $f[i]=\sum_{(i,j)\in E}{(f[j]+1)}$ 那么源点 f[1]即是答案.UPDATE:发现拓扑排序好不必要 直接倒着扫每一个点 因为此时必然存在严格的拓扑序 直接求方案数即可.

功能3 所有不同子串的长度 显然我们每走一步多加上一个字符 所以 ansx=anstn+dtn. 显然 当然还有类似递推公式这里不再赘述.

功能4 在所有原串所有子串中(相同的算一个)字典序第k大的是哪一个。这个问题的本质其实是第k大的路径 显然直接开始从根跑即可。复杂度是$|K|*|\sum{}|$ 其中$|\sum{}|$表示字符集的大小 这是最差复杂度.

功能5 考虑一下相同的算多次怎么 办我们显然可以发现 right集合表示了一个字符串可以出现的次数那么显然我们把这些也累加上 预处理出功能3的数组从根开始跑即可解决这个问题.

功能6 给定一个字符串S,找出字典序最小的循环移位.

广义后缀自动机:我也不知道是啥 就这样先叫吧 不管是啥 自己想的就行...(多个字符串共同使用一个后缀自动机从而解决多个字符串的问题.

大多数都是在说trie树上建SAM 其实和普通的一样不过在添加下一个字符串的时候要将last移到根节点的位置。

我们可以根据后缀自动机的实质来考虑这件事情 其实就是trie树嘛 不过这次trie树上所有重复的字串还是不会出现的因为前面有right集合的限制.

给定n个字符串 问每个字符串有多少子串是所有n个字符串中至少k个的子串包括本身 子串可以本质相同...

一旦和多个字符串联系在了 一起 我目前为止只学了三种做法一种是 把各个字符加上#然后对一个字符串建立后缀自动机。

另一种和这种类似进行求出后缀数组 LCP来搞一搞,还有一种是广义SAM.

第二种不再赘述太难写了,(后缀数组做事情就是ex,第一种还行,但这里主要是写广义SAM.

建出广义SAM之后考虑如何统计答案 子串的定义显然是所有前缀的后缀需要统计出每个由于right集是若干个等价类 这里的等价类是Trie树意义上的也就是说如果两个子串共用一个right集合的等价类说明当前子串在每一个字符串中都出现过 至此我们求出每一个状态即每一个right集合的也就是每一个节点出现的次数就再通过便利原字符串得到哪些状态是当前字符串的即可得到答案 这里存在一些case 如果一个字符串是符合要求的那么在parent树上的所有子串都是符合要求的。

求出每一个状态出现的次数 我们考虑暴力跳parent指针这样复杂度是$S^{\frac{2}{3}}$的复杂度 ,证明:咕咕咕...

接下来就靠个人操作了吧qwq...

关于品酒大会的做法:(以前用后缀数组写简直在ex人... 现在好了会后缀自动机了首先题目让求r相似的方案数 显然得是对于n杯酒我们对比的是对于一个字符串所有后缀的前缀是否相同从而确定是否是r相似的 而 parent树上两个节点的LCA定义的是两个字符串的最长相等后缀所以考虑将字符串翻转翻转之后我们parent树上的LCA处定义的是原字符串的所有后缀的前缀是否相同这恰好和题目中的意思相同,但是进一步的我们发现翻不翻转两者是等价的 也可以说是一一对应的但是标号不好区分这里最好是翻转一下不会那么迷...方案数显然可以dp一遍后缀树最大最小值也可以在dp的时候转移.

事实上 可以自己画一个简单的线段来模拟一下效果.

至此 duoxiao OJ上的SAM题被我做完了 我想我很了解SAM了 right集合往往利用线段树合并来实现这显然是必要的 毕竟bitset空间和时间复杂度过高了.

学到了一个更nb的操作 关于right集合大小可以建立虚树 dfs序维护 链加即可。

posted @ 2020-01-08 17:27  chdy  阅读(294)  评论(0编辑  收藏  举报