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

本文深入解析了后缀自动机(SAM)的基本概念、关键性质及其构建过程。SAM是一种优化后的自动机,用于高效处理字符串的所有后缀,特别适用于算法竞赛等场景。文中详细阐述了如何通过状态转移和子串长度范围来构建SAM,并提供了完整的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

一些分析和证明

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

下面有一个结论:
对于任意两个不同状态ST(s1),ST(s2),len(s1)<len(s2)ST(s1),ST(s2),len(s1)<len(s2) 都满足: Right(ST(s1))Right(ST(s1))Right(ST(s2))Right(ST(s2)) 要么没有交集,要么Right(ST(s1))Right(ST(s2))Right(ST(s1))⊃Right(ST(s2))
证明很简单,若 s1s1s2s2 的后缀,所有可能的rr是一样的,但由于 s2 长,会砍掉多一些,所以一定Right(ST(s1))Right(ST(s2))Right(ST(s1))⊃Right(ST(s2))
s1s1 不是 s2s2 的后缀,可能的 rr 完全不同,所以一定不交。

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

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

构造

SAMSAM 的构造是在线的过程。
假设我们现在已经构造出了串 TTSAM,现在要得到 TxTxSAMSAM :

现在我们新建了一个节点 pn=ST(Tx)pn=ST(Tx).
我们先找到所有状态中 RightRight 集合包含 len(T)len(T) 的节点 v1,v2,v3...v1,v2,v3...(只有这些节点可能向 pnpn 连边)。
ST(T)ST(T) 显然在其中,剩下的就是 ST(T)ST(T)ParentParent 树中的所有祖先。
不妨让他们从后代到祖先排为:v1=ST(T),v2,v3,...,vk=rootv1=ST(T),v2,v3,...,vk=root.

对于出发没有标号为 xx 的边的点 vi,说明需要直接连一条vivipnpn的标号为 xx 的边。
如果从 vi 出发有标号为 xx 的边,那么从 vi+1 出发之前肯定也有。(有标号为 xx 的边 RightRight 集合中存在T[r+1]=xT[r+1]=x)

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

栗子:( cljclj 巨神的 pptppt 上的)
A AAAAA xAAAAAAAAA AAAAA xAAAAAAAAB AAAAA x //vpvp
这时候其实就有两种状态了:
AAAAAAx AAAAAAAA AAAAAAx AAAAAAAABAAAAAx //qq
A AAAAAx AAAAAAAAA AAAAAx AAAAAAAAB AAAAAx //nq
实际上就是由于最后一个位置的限制,多出了一个 RightRight 变化的点。

当然如果 max(q)=max(vp)+1max(q)=max(vp)+1 ,即最后一个位置没有爆掉(把上面的 BB 改成 A ), 就没有必要新建状态。
直接让 Parent(np)=qParent(np)=q 即可结束这一阶段。

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

代码实现

我们先整理一下思路:
对于每一阶段(已经有 TTSAM ,求 TxTxSAMSAM) :
p=ST(T)p=ST(T) ,新建 np=ST(Tx)np=ST(Tx) .
ppParent 树上的所有祖先: v1=ST(T),v2,v3,...,vk=rootv1=ST(T),v2,v3,...,vk=root .
若之前 trans(vi,x)=nulltrans(vi,x)=null , 则更新为指向 npnp .
找到第一个原本trans(vp,x)=xtrans(vp,x)=xvpvp , 若找不到就结束该阶段。
否则,令 q=trans(vp,x)q=trans(vp,x).
max(q)=max(vp)+1max(q)=max(vp)+1,则更新 Parent(np)=qParent(np)=q,结束该状态。
否则,新建节点 nqnq,
复制之后的转移: trans(nq,)=trans(q,)trans(nq,∗)=trans(q,∗) .
更新: Parent(nq)=Parent(q),Parent(q)=nq,Parent(np)=nqParent(nq)=Parent(q),Parent(q)=nq,Parent(np)=nq
对于所有 vivitrans(vi,x)=qtrans(vi,x)=q,则更新 trans(vi,x)=nqtrans(vi,x)=nq
这一阶段结束。

写起来还是挺简单啊?

#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;
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值