什么是后缀自动机(SAM)?
大概可以理解成对暴力在字母树中插入n个后缀的一种优化。
首先它是一个自动机。
对于一个字符串
s
s
, 能识别其所有的后缀。还有一系列扩展运用。
一些分析和证明
用
ST(st)
S
T
(
s
t
)
表示在自动机中从初始状态沿着字符串st走到达的状态。
字符串
ST(a)
S
T
(
a
)
能识别
x
x
, 当且仅当 是
S
S
的后缀。
所以一个状态 能识别哪些后缀,只取决于
Right(a)
R
i
g
h
t
(
a
)
。
定义
Right(a)
R
i
g
h
t
(
a
)
表示
a
a
出现在 所以位置的右端点集合。
具体来说: 设
a
a
在 中出现的位置为
[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
)
。
我们把 {
r1−1,r2−1,…,rn−1
r
1
−
1
,
r
2
−
1
,
…
,
r
n
−
1
} 记为
Right(a)
R
i
g
h
t
(
a
)
。
字符串
x
x
能被识别,当且仅当 是母串
S
S
的后缀。
对于 ,适合他的子串的长度在一个范围内(子串太长
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
是一样的,但由于 长,会砍掉多一些,所以一定
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
完全不同,所以一定不交。
有了上面的结论我们可以画出一个 树,反映
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
,字符串不断短,刚刚小于 的时候,
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
的 ,现在要得到
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
v
i
到
pn
p
n
的标号为
x
x
的边。
如果从 出发有标号为
x
x
的边,那么从 出发之前肯定也有。(有标号为
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
的边的点。
考虑 ,设
q=trans(vp,x)
q
=
t
r
a
n
s
(
v
p
,
x
)
有
Right(trans(vp,x))={ri+1|ri∈Right(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
的中插入
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 //
实际上就是由于最后一个位置的限制,多出了一个
Right
R
i
g
h
t
变化的点。
当然如果
max(q)=max(vp)+1
m
a
x
(
q
)
=
m
a
x
(
v
p
)
+
1
,即最后一个位置没有爆掉(把上面的
B
B
改成 ), 就没有必要新建状态。
直接让
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
一样。
就相当于用 代替
q
q
,在 树中,把
q
q
踢下去一位,使 。
还没有更新完,我们还要把在
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
了, 树中
nq
n
q
在
q
q
上层。
哪些节点满足呢?
由于
vp,...,vk
v
p
,
.
.
.
,
v
k
都存在标号
x
x
的边,且 集合不断增大,所以满足
trans(vi,x)=q
t
r
a
n
s
(
v
i
,
x
)
=
q
的一定是只有一段:
vp,...ve
v
p
,
.
.
.
v
e
。把这些改一改即可。
貌似完了…
代码实现
我们先整理一下思路:
对于每一阶段(已经有
T
T
的 ,求
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
在 树上的所有祖先:
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;
}