什么是后缀自动机(SAM)?
大概可以理解成对暴力在字母树中插入n个后缀的一种优化。
首先它是一个自动机。
对于一个字符串 ss , 能识别其所有的后缀。还有一系列扩展运用。
一些分析和证明
用 ST(st)ST(st) 表示在自动机中从初始状态沿着字符串st走到达的状态。
字符串 ST(a)ST(a) 能识别xx , 当且仅当 是 SS 的后缀。
所以一个状态 能识别哪些后缀,只取决于 Right(a)Right(a)。
定义 Right(a)Right(a) 表示 aa 出现在 所以位置的右端点集合。
具体来说: 设 aa 在 中出现的位置为 [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) 。
我们把 {r1−1,r2−1,…,rn−1r1−1,r2−1,…,rn−1} 记为 Right(a)Right(a) 。
字符串 xx 能被识别,当且仅当 是母串 SS 的后缀。
对于 ,适合他的子串的长度在一个范围内(子串太长 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))。
证明很简单,若 s1s1 是 s2s2 的后缀,所有可能的rr是一样的,但由于 长,会砍掉多一些,所以一定Right(ST(s1))⊃Right(ST(s2))Right(ST(s1))⊃Right(ST(s2))。
若 s1s1 不是 s2s2 的后缀,可能的 rr 完全不同,所以一定不交。
有了上面的结论我们可以画出一个 树,反映 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 ,字符串不断短,刚刚小于 的时候, RightRight 集合就变成 Right(fa)Right(fa) 了。
ParentParent树的叶子结点是 O(n)O(n) 的,且每个非叶子节点至少有两个儿子(这里感觉不太对劲,比如aaaaaaaaaa,但是一时找不到好的解释,所以这里先假装他是对的好了,大神求教),所以自动机的点数是O(n)O(n) 的。
我们还需要证明边数的规模是线性的:
显然 SAMSAM 不是一棵树,所以我们建出 SAMSAM 的生成树。
对于每个后缀,沿着自动机走,将其对应上遇到的第一个非树边。
每个非树边至少被一个后缀所对应(假装它是对的),所以边数也是 O(n)O(n) 的。
构造
SAMSAM 的构造是在线的过程。
假设我们现在已经构造出了串 TT 的 ,现在要得到 TxTx 的 SAMSAM :
现在我们新建了一个节点 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 的边的点 ,说明需要直接连一条vivi到 pnpn的标号为 xx 的边。
如果从 出发有标号为 xx 的边,那么从 出发之前肯定也有。(有标号为 xx 的边 RightRight 集合中存在T[r+1]=xT[r+1]=x)
令 vpvp 表示 v1,...,vkv1,...,vk 中第一个出发有标号为 xx 的边的点。
考虑 ,设 q=trans(vp,x)q=trans(vp,x)
有Right(trans(vp,x))={ri+1|ri∈Right(vp) 且 T[ri+1]=x}Right(trans(vp,x))={ri+1|ri∈Right(vp) 且 T[ri+1]=x}.(这是更新之前的情况)
注意到我们直接在 qq 的中插入 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 //
实际上就是由于最后一个位置的限制,多出了一个 RightRight 变化的点。
当然如果 max(q)=max(vp)+1max(q)=max(vp)+1 ,即最后一个位置没有爆掉(把上面的 BB 改成 ), 就没有必要新建状态。
直接让 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一样。
就相当于用 代替 qq ,在 树中,把 qq 踢下去一位,使 。
还没有更新完,我们还要把在 vp,...,vkvp,...,vk 中原本trans(vi,x)=qtrans(vi,x)=q的改为 trans(vi,x)=nqtrans(vi,x)=nq, 因为我们已经用 nqnq 代替 qq 了, 树中 nqnq 在qq 上层。
哪些节点满足呢?
由于 vp,...,vkvp,...,vk 都存在标号 xx 的边,且 集合不断增大,所以满足 trans(vi,x)=qtrans(vi,x)=q 的一定是只有一段: vp,...vevp,...ve。把这些改一改即可。
貌似完了…
代码实现
我们先整理一下思路:
对于每一阶段(已经有 TT 的 ,求 TxTx 的 SAMSAM) :
令 p=ST(T)p=ST(T) ,新建 np=ST(Tx)np=ST(Tx) .
pp 在 树上的所有祖先: 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)=x 的 vpvp , 若找不到就结束该阶段。
否则,令 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
对于所有 vivi 若 trans(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;
}