很简单的后缀自动机(SAM)

版权声明:欢迎dalao指出错误暴踩本SD,蒟蒻写的博客转载也要吱一声 https://blog.csdn.net/enjoy_pascal/article/details/82429963

有关SAM什么的

我好像说过这辈子都是不可能学SAM的真香 主要是我不想初三退役
S A M SAM 后缀自动机,英文名Suffix Automaton,一种搞字符串的东西
**陈丽洁**在 2012 2012 年的冬令营上提出,从此 S A M SAM 变得广为人知,下面是 S A M SAM 的定义

  • 一个串 S S 的后缀自动机 ( S A M ) (SAM) 是一个有限状态自动机 ( D F A ) (DFA) ,它能且只能接受所有 S S 的后缀(当然,它能做的事情远远不仅限于后缀).

  • 后缀自动机其实是一个 D A G DAG (有向无环图),其中顶点是状态,边代表了状态之间的转移.

  • 某一状态 S S 被称为初始状态,由它能够到达其余所有状态.

  • 自动机的所有转移,都是一条有向边,且都被某种符号标记,从某一状态出发的所有转移必须拥有不同的标记.

  • 一个状态被称为终止状态,表示如果我们从初始状态 S S 经任意一条路径走到某一终止状态,并顺序写出经过边的标记,得到的字符串必定是原串的后缀.

  • 在符合上述条件的所有自动机中,后缀自动机拥有最少的状态与转移,并且后缀自动机的状态数以及转移数都是 O ( S ) O(|S|) 的.

以下是陈丽洁的《后缀自动机讲稿》
这里下载,aodq
不过非常不建议去看因为根本看不懂

这篇东西是对 S A M SAM 的一些感性总结,没有理性证明
所以很短


SAM的优点

其实 S A M SAM ……一言难尽

  • 后缀自动机,当然可以识别字符串的所有后缀

  • 时空复杂度都是 O ( n ) O(n) (没有证明)

  • 代码复杂度极低,码量令人发指地比 A C AC 自动机还短

  • 由于子串一定是某个后缀的前缀,所以 S A M SAM 也可以识别子串

  • 转移边的 D A G DAG 性质使在 S A M SAM D P DP 十分方便

  • p a r e n t parent 边可以套上许多数据结构(树剖、 L C T LCT )来维护,功能强大

  • 还有什么其他的我也不知道了


parent边和转移边

  • p a r e n t [ x ] parent[x] 指向的是字符串 [ S , x ] [S,x] 最长后缀 [ S , p a r e n t [ x ] ] [S,parent[x]] 的右端点

下面是字符串 a b a aba p a r e n t parent 边的栗子

比如字符串 a b a aba p a r e n t [ 3 ] parent[3] [ S , 3 ] [S,3] 的最长后缀 [ S , 1 ] [S,1] 的右端点 1 1

S A M SAM 里除了 p a r e n t parent 边,还有转移边(图中黄色的边是 p a r e n t parent 边,蓝色的是转移边)
每个点表示一个状态, S A M SAM 的转移则通过转移边实现


parent边的连边

对于 S A M SAM ,可以同时把转移边和 p a r e n t parent 边搞出来

  • 对于现在刚加入的 n o w now 点, l a s t last 按照字符串顺序走过来的前一个点, x x 代表新加入的字母

  • l a s t last 开始,一直向上找 l a s t last p a r e n t parent l a s t = p a r e n t [ l a s t ] last=parent[last]
    如果当前的某个 p a r e n t parent 没有一个儿子连向字母 x x ,我们就加上这条从 l a s t last n o w now 转移边,然后接着找 l a s t last p a r e n t parent

  • 直到这个点有儿子连向字母 x x ,我们就要分两种情况考虑了

举个栗子

这种情况 n o w = 3 now=3 ,新加入的字母是 a a
其实这张图还少了一条 S S 2 2 的转移边,字母为 b b

  • 设我们向上跳到了第一个符合条件的点是 p p 点, p p 点指向字母 x x 的儿子是 q q

  • 在这里 p p 点就是根 S S ,发现 p p 有指向字母 a a 的儿子节点 1 1 号节点(所以 q = 1 q=1

  • p p q q 之间的距离为 1 1 ,就可以直接把 p a r e n t [ n o w ] parent[now] 指向 q q (也就是 p a r e n t [ 3 ] = 1 parent[3]=1

但是如果 p p q q 点的距离大于 1 1 呢?比如


这种情况 n o w = 3 now=3 ,新加入的字母是 b b

此时 p p 点是根 S S q = 2 q=2 ,但是我们不能把 p a r e n t [ n o w ] parent[now] 赋值为 2 2 ,因为 a b ab 不是 a b b abb 的后缀

  • 连错误 p a r e n t parent 边的原因是我们把 a b ab 当成了 b b (因为从根 S S 2 2 有两条路)

  • 也就是误认为 a b ab a b b abb 的后缀

此时 S A M SAM 需要加点操作


加点操作

  • 我们新加一个点,从 p p 到新点、从新点到 n o w now 都连上字母 x x 的转移边

  • 注意这个新点不存在原串里,只是 q q 的一个分身虚点,所以 q q 点的所有转移边信息全部复制给新点

  • 新点的 p a r e n t parent 即为 q q 原来的 p a r e n t parent q q n o w now p a r e n t parent 边都重新指向新点

  • 除此之外我们还要继续向 p a r e n t parent 边回溯直到根 S S ,把所有指向 q q 的转移边重新指向新点

对于上面 a b b abb 的情况,应该这样连边

最终 S A M SAM p a r e n t parent 边性质没有被破坏,加点操作正确


code&其他

  • 这样的话 S A M SAM 就建完了
void sam(int x)
{
    int p;
    len[++tot]=len[last]+1,sum[tot]=1,now=tot;
    for (p=last;p && !son[p][x];p=parent[p])son[p][x]=now;
    if (p)
    {
        int q=son[p][x];
        if (len[q]>len[p]+1)
        {
            len[++tot]=len[p]+1;
            memcpy(son[tot],son[q],sizeof(son[q]));
            parent[tot]=parent[q];
            parent[q]=parent[now]=tot;
            for (;son[p][x]==q;p=parent[p])son[p][x]=tot;
        }
        else parent[now]=q;
    } 
    else parent[now]=1;
    last=now;
}
  • 再再比如说 a a b b a b d aabbabd S A M SAM 是这样的


SAM的一些应用

S A M SAM 可是很有用的

询问一个串 T T 是否是一个串 S S 的子串

  • S S S A M SAM

  • 然后 T T S A M SAM 上面跑

询问一个串 S S 有多少个不同的子串

  • S S S A M SAM

  • 从根 S S 出发的任意一条转移边路径都是一个不同的子串

  • 答案即为根 S S 出发不同路径数量


#其实SAM真的很简单……

本人版权意识薄弱……

猜你喜欢

转载自blog.csdn.net/enjoy_pascal/article/details/82429963