前言(=废话)
之前听zrt大佬讲过一次后缀自动机,当时他重复讲了好久我还是没听懂;
最近想平复一下心情,于是选择硬啃一波后缀自动机;
然后因为这玩意真特么难,打算写一篇正式点的笔记记录一下。。。
当然因为个人能力原因,只能用一些形象化的语言来描述一下,并且不保证都是对的。。。只是便于理解;
并且我不会去讲理论复杂度以及原理那一套,我相信别人讲得比我好得多orz,然后我就口胡一波我理解的构造过程把。。。
以下后缀自动机简称为sam
定义
sam的定义有一大坨,然后那一套理论我并不很懂;
所以我就说一下sam建出来之后需要满足什么样子吧。。。
- 首先sam是一个trie图,每个点最多有字符集大小的出边,每条边上代表一个字母,并且是个DAG;
- 在一个串的sam上dfs,将根节点到每个点的所有不同路径上的字符输出,会不重不漏地输出这个串的所有不重复子串(这也是为什么sam是处理子串问题的有力武器);
- 注意上一条的不重不漏!!!这在之后会反复提到;
- sam的点数是O(n)级别的,相应的,边数最多也就字符集大小*n,这保证了其空间复杂度;
- 每个点有一个len值,代表从根节点到这个点最长的路径长度;
- 每个点有一个父节点,设父节点的len值为flen,那么从根节点到这个点的所有路径长度是取遍(flen,len](左开右闭)的所有值的;
- 每个点到根节点的所有路径不光长度是连续的,并且长度小的永远是长度大的后缀(相当于从长为len的子串开始每次从前端减少一个字符直到flen;
一些重要变量的定义
len[i]:节点i到根节点的所有路径中长度最长的路径的长度;
f[i]:满足其到根节点的最长路径是i的最短路径减一的点,且其所有路径是都是i的所有路径的后缀(路径代表一个子串)
构建
记住上边的那些性质,然后就可以来构建了;
考虑使用增量法,每次往已经构建好的sam中添加一个字符,相当于在字符串末尾依次添加;
首先如果当前是空集,那么肯定是符合上述所说的所有性质的;
考虑已经建好了一个满足上述的所有性质的sam,现在的字符串为S,要在末尾添加一个x;
首先建一个新节点,因为当前的sam中最长的肯定是一个长为|S|的路径,然后新加一个字符的话肯定需要有一条路径来表示整个串,那个路径的长度就是|S|+1;
设插入前长度为|S|的末尾的节点为p,直接将p指向新节点即可;
然后沿着f[p]往上跳,考虑要想使新插入的这个节点满足上述所有性质,f[p]向上跳过程中每一个点都必须要有一个后继节点为x;
如果没有,直接连向新建的节点即可;
因为f[p]的每个节点其所有路径代表的子串是没有重复的,也正好取遍所有(flen,len]的长度;
那么新节点其len值即为len[p]+1,所能代表的路径长度范围是所有向上跳的过程中能够连向新节点的那些点的最短路径+1;
但是一旦有一个节点有后继x节点,那么再往上跳肯定都会有后继为x的节点(具体为什么啃一啃大佬们的论文就知道啦)
设这个后继的x节点为q,当前跳到的点设为fp;
如果q的len值仍为当前跳到的fp节点的len值+1,很显然再往上跳的后继x节点都会满足6,7的条件。此时将新节点的f设为q即可,那么新节点就会满足6,7等性质了;
但如果q的len值比当前跳到的len值+1还要大(不可能比其len值+1小,因为len值是最长路径长度,既然有边连过来那就肯定最长长度要大一些)考虑是由什么原因造成的?
因为sam要不重不漏的表示所有不重复子串,那么肯定q的所有路径中有一部分的前端多出来一些;
那么就要把这个q点给分裂一下,一部分满足新的需求,同时保证原来的性质不变;
现在新建一个nq节点,nq需要拷贝一下所有q的后继节点;
然后nq的len值设为所需的len[fp]+1,此时len[nq] < len[q],而nq又是从q分出一部分的,nq所有路径都是q的所有路径的后缀,所以f[q]=nq,而f[nq]设为之前的f[q],新节点的f也设为nq(nq是专门分裂出来满足新节点的嘛。。。);
然后把所有符合新的节点条件的那些路径都改给nq,因为不会有重复的子串路径,所以沿着f[p]往上跳,所有后继x节点为q的都改成nq即可;
感觉我就是记了个流水账。。。表达能力好差。。。而且现在理解也不是非常深,可能以后还要再来填坑?。。。
应用
先去水题以后补