[置顶] 后缀自动机(SAM)学习笔记

构图及原理

定义

算法

后缀自动机(SAM)就是一个要实现能存下一个串中所有子串的算法,按一般来说应当有 O(N2) 个状态,而SAM却可以用O(N)个状态来表示所有子串,因为它把很多个本质相似的子串映射到了同一个状态上,从而实现了这个优美的算法。

  1. S :原串,我们要求的就是 S 的后缀自动机。
  2. Rightstr :表示 str 在母串 S 中所有出现的结束位置的集合。
  3. s (状态):表示所有 Right 集相同的字符串合成的状态。
  4. Parents :表示 Right 集包含了 Rights 并且集合最小的状态。设转移的表示为 trans()=

  1. parent 边:一个状态指向它的 Parents 。(而由 parent 边构成的树可以称为 parent 树)
  2. a ~ z (或某些字符)字符边:有一个状态,后面加上一个字符后所指向的状态。

注意:
1. 一个状态可以由多条字符边转移而来,因为它包含多个子串,但只能有一条 Parent 边转移来。
2. 一个状态连出去的字符边不一定所有包含的子串后都能接这个字母,只是代表有某些包含的子串后能接这个字母。

一些简单的性质

1. Right 集的性质

  1. 对于两个子串 a , b Right ,只有两种情况,有交集和没有交集。考虑有交集的情况,如果有交集,那么显然一个子串是另一个子串的后缀,设 a b 的后缀,那么 RightbRighta 即对于两个子串的 Right 要么包含,要么没有交集。所以对于一个状态 s 所表示的字符串,它们的右端点必定相同,而左端点必定是连续的一段。如下图:
    [置顶] 后缀自动机(SAM)学习笔记_第1张图片
  2. 如果一个状态的 Right 集越大,就说明符合的子串越多,那么限制肯定就越小,所以左端点距右端点的距离就越小。而随着右端点的向右移动,符合一种条件的子串就越来越少,所以 Right 集就会变小(当然可能会分出两组不同的状态)。我们令一个状态 s 所表示的区间是 [Mins,Maxs] ,显然的 Mins1=MaxParents ,对于一个状态,我们记 Lens 表示 Maxs

2. Parent 树的性质

  1. 怎么理解 Parent 树?我们从叶子结点往根上走时,就是一些不相交的 Right 集不断合并的过程(因此我们不需要把 Right 集的全部节点存下来)。
  2. 如果 trans(s1,c)=trans(s2,c)=trans(s3,c)....=t ,那么状态 s1,s2,s3... Parent 树上肯定是在一段连续的链上的。因为在这些子串的右端点加上一个字符 c 后,它们的 Right 集又重新相等,证明它们本来就是包含且连续的关系。
  3. Parent 边简单来说,即表示不断寻找后缀的过程

构造方法

注:这里很多推理都是有上面的性质得来的,如有不理解的可以再回顾一下性质。

假设我们已经完成了前 |S|1 个字符的插入(为 T 串),现在插入第 |S| 个( c )字符(为 S 串)。
要想SAM继续保持它的功能,就要加入 S 串的所有后缀。而 S 串的所有后缀都可以由 T 的后缀加一个字符转移而来。由于必定存在一个 Rightx 仅为 |T| 的状态。并且根据 Parent 树的性质,它的祖先的 Right 集肯定也含有 |T| ,所以它们肯定构成了一条在 Parent 树中的链。所以我们只要沿着这条链就能找到所有 T 的后缀。而我们讨论一下在 Parent 链上走的意义,即不断跳到当前字符串的后缀(当我们走完这条 Parent 链时就意味着|T|的所有后缀都遍历完了)
在沿着这条链走的时候会出现两种情况,假设到达的状态是 x
(我们新加一个节点 np ,表示 Right 只含 |S| 的新状态。即 lennp=|S|

  1. trans(x,c)=Null :即当前这个状态没有沿 c 转移的边,所以我们只要连一条为 c 的字符边到 np 就处理完这种情况了。

  2. trans(x,c)Null :假设第一个找到的节点为 x ,转移到的节点为 q 。那我们怎么把 |S| 这个位置加入 Rightq ?我们发现,如果强行把 |S| 加入 Rightq 中,可能会使 q 节点出现矛盾,即 lenq 变小。我们来看一下下面这个例子:
    [置顶] 后缀自动机(SAM)学习笔记_第2张图片
    如图,如果 lenq=lenx+1 那么就不会有这种问题,直接让 Parentnp=q 就可以了。但如果 lenq>lenx+1 ,详细的说,就意味着有更多的子串共享 q 这一个状态,如果 |S| 加进当前状态的 Right 集,可能会出现这个状态中的一些子串与到达 |S| 这个结束端点矛盾。如上面一些蓝色的串就不能放在后缀为 |S| 的位置,会与 B 字符矛盾。那么讨论就要复杂点。我们可以把 q 分成两种状态。如下图。
    [置顶] 后缀自动机(SAM)学习笔记_第3张图片
    如图,我们可以把 q 串分出一个 nq 来解决这个问题,只要我们把它再细分化,把符合结束位置为 |S| 的子串和不符合的分开。就可使 nq Right 集包含 |S| ,从而解决这个问题。那么显然 lennq=lenx+1 ,由于 nq q 分出来的一个 Rightnq 包含 Rightx 的状态,所以自然 Parentq=nq,Parentnq=x ,当然也要使 Parentnp=nq 。并且考虑 nq 字符边的转移,由于结束位置为 |S| 的子串后是没有字符的所以它的转移状态是和 q 一样的。

注意第一种情况一定是在第二种情况前出现的,而第二种情况就意味着一段段后缀的处理。

最后,我们还要处理别连向 q 的状态,可以知道的是它们是连续一段的,所以把它们转移到 q 的边都改为转移到 nq 的边就可以了。而对于其它有 c 的字符边,且指向状态不为 q 的,由于 q 都已经能分离出合法状态了,那么那些节点的 Right 集都是可以直接加入 |s| 。至此,我们就已经把 T 的所有后缀都建立了转移到 S 后缀的方案。

程序

非常好实现。

//Suffix Automaton’s Build YxuanwKeith
//S为根,一开始tot = 1表示已经给根编号为1
void Add(int c) {
    int Nt = ++ tot, p = Last;
    //Last表示前缀T的最长后缀对应的状态。
    Tr[Nt].Len = Tr[Last].Len + 1, Last = Nt;
    for (; p && !Tr[p].Go[c]; p = Tr[p].Pre) Tr[p].Go[c] = Nt;
    if (!p) Tr[Nt].Pre = S; else {
        int q = Tr[p].Go[c];
        if (Tr[p].Len + 1 == Tr[q].Len) Tr[Nt].Pre = q; else {
            int Nq = ++ tot;
            Tr[Nq] = Tr[q];
            Tr[q].Pre = Tr[Nt].Pre = Nq;
            Tr[Nq].Len = Tr[p].Len + 1;
            for (; p && Tr[p].Go[c] == q; p = Tr[p].Pre) Tr[p].Go[c] = Nq;
        }
    }
}

例题及应用(未完成)

未完待续……

参考资料

  1. 张天扬《后缀自动机及其应用》(这个可以作为辅助资料)
  2. 陈立杰《Suffix Automaton后缀自动机》(这个讲的比较详细,容易理解)

你可能感兴趣的:(算法)