后缀自动机学习笔记

  参考:https://blog.csdn.net/doyouseeman/article/details/52245413

     https://wenku.baidu.com/view/90f22eec551810a6f4248606.html

     APIO2018课件《从DFA到后缀自动机》张云帆

  随便写一点。

  定义一个子串的right集合为这个子串在原串中出现的右端点集合。

  SAM作为一个自动机,自然是一个DAG。其中每个点表示一个right集合。边表示“给其起点的right集合所表示的子串‘添上该边所表示的字符’能够扩展到其终点的right集合”。令len表示一个点表示的最长子串长度。

  可以发现原串中会有一些不同的子串有相同的right集合,若其相同,一定存在某子串是另一子串的后缀。并且如果给他们添上同一个字符,能拓展到的状态是相同的。

  并且还会有一些点的right集合被另一些点包含。若其包含,则显然right集合元素个数较多的点表示的子串为被包含点的后缀。

  类似于AC自动机,给SAM定义fail指针。令每个点的fail指针指向“所有包含其right集合的点中‘right集合元素个数最少’的点”,若不存在则指向初始状态(空串,其right集合可视为0~n)。显然被包含点所表示的最短子串的长度=fail指向点表示的最长子串的长度+1。如果在某个点无法继续匹配下去,就可以跳转到fail指向的点,因为其right集合更大,选择更多。

  将fail指针反向后形成的树称为parent树。因为对于parent树上的点,其父亲表示的子串为其后缀,那么这棵parent树其实就是将原串的所有前缀取出并翻转后建出的trie,更简单一点地看就是原串的反串的后缀树。并且parent树上两个子串的最长公共后缀就是其lca表示的子串中最长的。

  接下来考虑构造SAM。构造算法是在线的,即在构造了i-1前缀的SAM的基础上插入s[i]。

  为s[i]新建一个节点x,表示right集合仅为i的子串。由于SAM实际上是不记录right的,如果我们要统计每一个点right集合的大小,在这里将这些点的size设为1,最后再用parent树统计子树内size和即可。虽然在之后的更新中其不一定会表示right集合仅为i的子串,但它曾经表示过,那么就可以用它统计。

  考虑将x加入SAM中。将上一个这样的节点(即表示right集合仅为i-1的节点)称为last。显然需要由last向x连一条边,表示添加上字符s[i]后可以从last转移到x。

  然后继续考虑还有哪些状态可以转移到x。显然既然这样的状态表示的子串加上s[i]后可以成为一个i前缀的后缀,那么当前其就是i-1前缀的后缀,即right集合包含i-1,而right集合包含i-1的点一定是last的祖先。

  于是对于这些点,如果当前其没有s[i]边,给它添加s[i]边连向x;否则此时其s[i]边指向的点的right集合相当于已包含了i,不用再连边。并且显然若某个点存在s[i]边,其祖先也一定存在s[i]边。那么从last节点沿着fail指针不断往上跳并向x连边,直到到达存在s[i]边的节点时停止(或根)就可以了。

  对于原本就已有s[i]边的点,其s[i]边所连向点的right集合应已包含i,但还并没有对其做出修改。设第一个找到的存在s[i]边的节点为p,其s[i]边所连向点为q。有lenq>lenp,因为q的right集合显然在p的right集合+1中。

  考虑给q的right集合中加入i造成的影响。可以发现当新加入的后缀子串无法满足len的限制时,这会造成q状态的分裂。分两种情况。

  若lenq=lenp+1,则其不会分裂。这显然成立。q的right集合是除x外最小的包含i的right集合,因为p的祖先的right集合比p更大,后继状态的right集合也会更大。于是可以把x的fail指针指向q。同样由于这一点,对于p的祖先的后继状态不会产生影响,因为其right包含了q的right且len更短。

  而若lenq>lenp+1,就需要进行分裂了。显然其后缀添加s[i]后只有len<=lenp+1的部分能与其他子串相同,否则与p的right集合是极小的真包含i的集合矛盾。于是以子串长度小于等于lenp+1(包含后缀)和大于lenp+1(不包含后缀)为界线分割。对于小于等于lenp+1的部分,新建点y,将leny设为lenp+1,其余与q相同。而大于lenp+1的部分留在q点。然后考虑fail指针。由于y的right集合仅比q多了一个i,q的fail指针指向y。同时与之前的情况类似,x的fail指针也指向y。y的fail指针指向原q的fail指针所指向点,因为它们的right同时添加了i。之后需要将p及其祖先中指向q的点改为指向y。感觉这里有一堆搞不懂为什么的东西不过可能搞懂了也没啥用那就弃疗吧

  关于广义后缀自动机,见https://www.cnblogs.com/Gloid/p/10827640.html

  模板:

//初始时cnt=last=1
void
ins(int c) { int x=++cnt,p=last;last=x;len[x]=len[p]+1; while (!son[p][c]&&p) son[p][c]=x,p=fail[p]; if (!p) fail[x]=1; else { int q=son[p][c]; if (len[q]==len[p]+1) fail[x]=q; else { int y=++cnt; len[y]=len[p]+1; memcpy(son[y],son[q],sizeof(son[q])); fail[y]=fail[q],fail[x]=fail[q]=y; while (son[p][c]==q) son[p][c]=y,p=fail[p]; } } }

 

posted @ 2018-08-22 23:05  Gloid  阅读(305)  评论(0编辑  收藏  举报