SAM 后缀自动机——学习笔记

什么是后缀自动机(SAM)?

大概可以理解成对暴力在字母树中插入n个后缀的一种优化。
首先它是一个自动机。
对于一个字符串 s s , SAM 能识别其所有的后缀。还有一系列扩展运用。

一些分析和证明

ST(st) S T ( s t ) 表示在自动机中从初始状态沿着字符串st走到达的状态。
字符串 ST(a) S T ( a ) 能识别 x x , 当且仅当 ax S S 的后缀。
所以一个状态 ST(a) 能识别哪些后缀,只取决于 Right(a) R i g h t ( a )
定义 Right(a) R i g h t ( a ) 表示 a a 出现在 S 所以位置的右端点集合。
具体来说: 设 a a S 中出现的位置为 [l1,r1),[l2,r2),,[ln,rn) [ l 1 , r 1 ) , [ l 2 , r 2 ) , … , [ l n , r n ) , ST(a) S T ( a ) 就能够识别 Suffix(r1),Suffix(r2),,Suffix(rn) S u f f i x ( r 1 ) , S u f f i x ( r 2 ) , … , S u f f i x ( r n )
我们把 { r11,r21,,rn1 r 1 − 1 , r 2 − 1 , … , r n − 1 } 记为 Right(a) R i g h t ( a )
字符串 x x 能被识别,当且仅当 x 是母串 S S 的后缀。
对于 Right(a),适合他的子串的长度在一个范围内(子串太长 Right R i g h t 集合边小,太短 Right R i g h t 集合变大), 记作 [min(a),max(a)] [ m i n ( a ) , m a x ( a ) ] .

下面有一个结论:
对于任意两个不同状态 ST(s1),ST(s2),len(s1)<len(s2) S T ( s 1 ) , S T ( s 2 ) , l e n ( s 1 ) < l e n ( s 2 ) 都满足: Right(ST(s1)) R i g h t ( S T ( s 1 ) ) Right(ST(s2)) R i g h t ( S T ( s 2 ) ) 要么没有交集,要么 Right(ST(s1))Right(ST(s2)) R i g h t ( S T ( s 1 ) ) ⊃ R i g h t ( S T ( s 2 ) )
证明很简单,若 s1 s 1 s2 s 2 的后缀,所有可能的 r r 是一样的,但由于 s2 长,会砍掉多一些,所以一定 Right(ST(s1))Right(ST(s2)) R i g h t ( S T ( s 1 ) ) ⊃ R i g h t ( S T ( s 2 ) )
s1 s 1 不是 s2 s 2 的后缀,可能的 r r 完全不同,所以一定不交。

有了上面的结论我们可以画出一个 Parent 树,反映 Right R i g h t 集合的包含关系。
Parent P a r e n t 树从上往下 Right R i g h t 集合变小,子串长度变长。
fa=Parent(a)Right(a)Right(fa) f a = P a r e n t ( a ) ⇒ R i g h t ( a ) ⊂ R i g h t ( f a ) Right(fa) R i g h t ( f a ) 最小。
发现 Max(fa)=Min(a)1 M a x ( f a ) = M i n ( a ) − 1 。因为对于 a a ,字符串不断短,刚刚小于 Min(a) 的时候, Right R i g h t 集合就变成 Right(fa) R i g h t ( f a ) 了。
Parent P a r e n t 树的叶子结点是 O(n) O ( n ) 的,且每个非叶子节点至少有两个儿子(这里感觉不太对劲,比如 aaaaa a a a a a ,但是一时找不到好的解释,所以这里先假装他是对的好了,大神求教),所以自动机的点数是 O(n) O ( n ) 的。

我们还需要证明边数的规模是线性的:
显然 SAM S A M 不是一棵树,所以我们建出 SAM S A M 的生成树。
对于每个后缀,沿着自动机走,将其对应上遇到的第一个非树边。
每个非树边至少被一个后缀所对应(假装它是对的),所以边数也是 O(n) O ( n ) 的。

构造

SAM S A M 的构造是在线的过程。
假设我们现在已经构造出了串 T T SAM,现在要得到 Tx T x SAM S A M :

现在我们新建了一个节点 pn=ST(Tx) p n = S T ( T x ) .
我们先找到所有状态中 Right R i g h t 集合包含 len(T) l e n ( T ) 的节点 v1,v2,v3... v 1 , v 2 , v 3 . . . (只有这些节点可能向 pn p n 连边)。
ST(T) S T ( T ) 显然在其中,剩下的就是 ST(T) S T ( T ) Parent P a r e n t 树中的所有祖先。
不妨让他们从后代到祖先排为: v1=ST(T),v2,v3,...,vk=root v 1 = S T ( T ) , v 2 , v 3 , . . . , v k = r o o t .

对于出发没有标号为 x x 的边的点 vi,说明需要直接连一条 vi v i pn p n 的标号为 x x 的边。
如果从 vi 出发有标号为 x x 的边,那么从 vi+1 出发之前肯定也有。(有标号为 x x 的边 Right R i g h t 集合中存在 T[r+1]=x T [ r + 1 ] = x )

vp v p 表示 v1,...,vk v 1 , . . . , v k 中第一个出发有标号为 x x 的边的点。
考虑 Right(vp)={r1,r2,...rn} ,设 q=trans(vp,x) q = t r a n s ( v p , x )
Right(trans(vp,x))={ri+1|riRight(vp)  T[ri+1]=x} R i g h t ( t r a n s ( v p , x ) ) = { r i + 1 | r i ∈ R i g h t ( v p )   且   T [ r i + 1 ] = x } .(这是更新之前的情况)
注意到我们直接在 q q Right中插入 len(T)+1 l e n ( T ) + 1 可能会炸掉:最后一个串 [l,len(T)+1] [ l , l e n ( T ) + 1 ] 不一定对,可能导致 max(q) m a x ( q ) 变小,所以就多了一个状态了。
我们建一个新状态为 nq n q , Right(nq)=Right(q){len(T)+1} R i g h t ( n q ) = R i g h t ( q ) ∪ { l e n ( T ) + 1 }

栗子:( clj c l j 巨神的 ppt p p t 上的)
A AAAAA xAAAAAAAAA AAAAA xAAAAAAAAB AAAAA x // vp v p
这时候其实就有两种状态了:
AAAAAAx AAAAAAAA AAAAAAx AAAAAAAABAAAAAx // q q
A AAAAAx AAAAAAAAA AAAAAx AAAAAAAAB AAAAAx //nq
实际上就是由于最后一个位置的限制,多出了一个 Right R i g h t 变化的点。

当然如果 max(q)=max(vp)+1 m a x ( q ) = m a x ( v p ) + 1 ,即最后一个位置没有爆掉(把上面的 B B 改成 A ), 就没有必要新建状态。
直接让 Parent(np)=q P a r e n t ( n p ) = q 即可结束这一阶段。

若需要新建节点 nq n q , 可以发现, Parent(nq)=()Parent(q)Parent(q)=nq,Parent(np)=nq P a r e n t ( n q ) = ( 原 ) P a r e n t ( q ) P a r e n t ( q ) = n q , P a r e n t ( n p ) = n q .
由于 nq n q 之后的转移和 {len(T)+1} { l e n ( T ) + 1 } 无关(没有下一位),所以 nq n q 之后的所有转移和 q q 一样。
就相当于用 nq 代替 q q ,在 Parent 树中,把 q q 踢下去一位,使 Parent(q)=nq
还没有更新完,我们还要把在 vp,...,vk v p , . . . , v k 中原本 trans(vi,x)=q t r a n s ( v i , x ) = q 的改为 trans(vi,x)=nq t r a n s ( v i , x ) = n q , 因为我们已经用 nq n q 代替 q q 了, Parent 树中 nq n q q q 上层。
哪些节点满足trans(vi,x)=q呢?
由于 vp,...,vk v p , . . . , v k 都存在标号 x x 的边,且 Right 集合不断增大,所以满足 trans(vi,x)=q t r a n s ( v i , x ) = q 的一定是只有一段: vp,...ve v p , . . . v e 。把这些改一改即可。
貌似完了…

代码实现

我们先整理一下思路:
对于每一阶段(已经有 T T SAM ,求 Tx T x SAM S A M ) :
p=ST(T) p = S T ( T ) ,新建 np=ST(Tx) n p = S T ( T x ) .
p p Parent 树上的所有祖先: v1=ST(T),v2,v3,...,vk=root v 1 = S T ( T ) , v 2 , v 3 , . . . , v k = r o o t .
若之前 trans(vi,x)=null t r a n s ( v i , x ) = n u l l , 则更新为指向 np n p .
找到第一个原本 trans(vp,x)=x t r a n s ( v p , x ) = x vp v p , 若找不到就结束该阶段。
否则,令 q=trans(vp,x) q = t r a n s ( v p , x ) .
max(q)=max(vp)+1 m a x ( q ) = m a x ( v p ) + 1 ,则更新 Parent(np)=q P a r e n t ( n p ) = q ,结束该状态。
否则,新建节点 nq n q ,
复制之后的转移: trans(nq,)=trans(q,) t r a n s ( n q , ∗ ) = t r a n s ( q , ∗ ) .
更新: Parent(nq)=Parent(q),Parent(q)=nq,Parent(np)=nq P a r e n t ( n q ) = P a r e n t ( q ) , P a r e n t ( q ) = n q , P a r e n t ( n p ) = n q
对于所有 vi v i trans(vi,x)=q t r a n s ( v i , x ) = q ,则更新 trans(vi,x)=nq t r a n s ( v i , x ) = n q
这一阶段结束。

写起来还是挺简单啊?

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct node{
    node *par,*ch[26];
    int _max;
    node(int t1=0){ par=0; _max=t1; memset(ch,0,sizeof(ch)); }
} *root, *last;
typedef node* P_node;
void Extend(char x){
    P_node p=last, np=new node(p->_max+1);
    while(p&&p->ch[x]==0) p->ch[x]=np, p=p->par;
    if(!p) np->par=root; else{
        P_node q=p->ch[x]; 
        if(q->_max==p->_max+1) np->par=q; else{
            P_node nq=new node(p->_max+1);
            for(int i=0;i<=25;i++) nq->ch[i]=q->ch[i];
            nq->par=q->par; q->par=nq; np->par=nq;
            while(p&&p->ch[x]==q) p->ch[x]=nq, p=p->par;
        }
    }
    last=np;
}
char st[1000005];
int main(){
    freopen("sam.in","r",stdin);
    freopen("sam.out","w",stdout);
    root=last=new node(0);
    scanf("%s",st+1); 
    int len=strlen(st+1);
    for(int i=1;i<=len;i++) Extend(st[i]);
    return 0;
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值