所谓后缀自动机,就是通过后缀建立的自动机
(逃)
请允许我先介绍一下后缀家族:
(又逃)
前言
OI生涯目前为止学习的最为难以理解的算法,没有之一。
到现在也没有完全的理解。
qwq
概念
定义:
后缀 i i i :字符串 s s s 以 i i i 结尾的后缀(前缀同理)
e n d p o s ( x ) endpos(x) endpos(x) 字符串 x x x 在 s s s 中出现的结尾位置的集合
等价类:若 e n d p o s ( u ) = e n d p o s ( v ) endpos(u)=endpos(v) endpos(u)=endpos(v),我们就称 u u u 和 v v v 属于同一个等价类
不难发现,对于
s
s
s 的一个子串
x
=
s
l
.
.
.
r
x=s_{l...r}
x=sl...r,都存在一个位置
p
∈
(
l
,
r
]
p\in (l,r]
p∈(l,r],满足对于
l
≤
i
≤
p
l\le i\le p
l≤i≤p,
x
x
x 的后缀
i
i
i 都与
x
x
x 属于同一个等价类,而对于
p
<
i
≤
r
p<i\le r
p<i≤r,后缀
i
i
i 与
x
x
x 都不属于一个等价类。
我们称满足这个性质的
p
p
p 为
l
i
n
k
(
x
)
link(x)
link(x)。
SAM是一个字符转移边组成的DAG,每条从根结点出发的路径都唯一对应 s s s 的一个子串,在SAM中,每个结点都对应着一个等价类的集合(也就是从根结点到该结点的路径所代表的字符串的集合,这些字符串必然由一个最长的字符串和它的一些连续的后缀组成),一个结点的 l i n k link link 定义为该结点对应的最长字符串的 l i n k link link。
请务必确保你理解了上面这段话
构建
现在考虑如何构建出SAM
使用增量法,当前加入一个新的字符串
c
c
c
设上一个加入的结点为
l
s
t
lst
lst,当前加入结点为
c
u
r
cur
cur
设一个结点代表的最长字符串的长度为
l
e
n
len
len
首先,令
l
e
n
(
c
u
r
)
←
l
e
n
(
l
s
t
)
+
1
len(cur)\gets len(lst)+1
len(cur)←len(lst)+1
然后从
l
s
t
lst
lst 沿着
l
i
n
k
link
link 不断往上跳,直到跳到某个有
c
c
c 的转移边或者跳到根为止,沿途把
c
c
c 的转移边全部赋值成
c
u
r
cur
cur
situation 1
若到根了还没有 c c c 的转移边:说明整个字符串还没有出现过 c c c,直接把 l i n k ( c u r ) link(cur) link(cur) 赋值成根即可
否则,设跳到了结点
p
p
p,
p
p
p 的
c
c
c 转移边为
q
q
q
由于一直跳的是
situation 2
若 l e n ( q ) = l e n ( p ) + 1 len(q)=len(p)+1 len(q)=len(p)+1:这两个结点在原串上就是相邻的,直接令 l i n k ( c u r ) = l i n k ( q ) link(cur)=link(q) link(cur)=link(q) 即可
situation 3
若 l e n ( q ) ≠ l e n ( p ) + 1 len(q)\ne len(p)+1 len(q)=len(p)+1:这两个结点在原串上不是相邻的,此时若按照情况2的处理方法,会使SAM上出现不应该出现的前缀,所以我们应该分裂出一个结点 p p pp pp,继承所有 q q q 的信息, l e n ( p p ) ← l e n ( p ) + 1 len(pp)\gets len(p)+1 len(pp)←len(p)+1,并把 q q q 和 c u r cur cur 的 l i n k link link 全指向 p p pp pp,再一路往上把本来连向 q q q 的转移连向 p p pp pp
代码
void ins(int c){
c-='a';
int cur=++tot,p=lst;lst=tot;
st[cur].len=st[p].len+1;siz[cur]=1;
for(;p&&!st[p].tr[c];p=st[p].fa) st[p].tr[c]=cur;
if(!st[p].tr[c]) st[cur].fa=1;
else{
int q=st[p].tr[c];
if(st[q].len==st[p].len+1) st[cur].fa=q;
else{
int pp=++tot;st[pp]=st[q];
st[pp].len=st[p].len+1;
st[q].fa=st[cur].fa=pp;
for(;p&&st[p].tr[c]==q;p=st[p].fa) st[p].tr[c]=pp;
return;
}
}
}
应用
求 endpos 集合大小
定义
s
i
z
x
siz_x
sizx 为结点
x
x
x 的等价类集合中
e
n
d
p
o
s
endpos
endpos 的数目。(也就是出现次数)
那么根据定义,有:
s
i
z
x
=
∑
s
∈
s
o
n
x
s
i
z
s
+
[
x
∈
S
]
siz_x=\sum_{s\in son_x} siz_s+[x\in S]
sizx=s∈sonx∑sizs+[x∈S]
其中
S
S
S 是每次插入的终点集合
dfs或者拓扑实现均可
int cnt[N],id[N];
void calc(){
for(int i=1;i<=tot;i++) ++cnt[st[i].len];
for(int i=1;i<=n;i++) cnt[i]+=cnt[i-1];
for(int i=tot;i>=1;i--) id[cnt[st[i].len]--]=i;
for(int i=tot;i>=1;i--) siz[st[id[i]].fa]+=siz[id[i]];
return;
}
求本质不同子串数
就是在自动机上的走法种类呗。
那么就有:
s
u
m
x
=
∑
s
=
t
r
x
,
c
s
u
m
s
+
1
sum_x=\sum_{s=tr_{x,c}}sum_s+1
sumx=s=trx,c∑sums+1
Thanks for reading!
后缀树
本身也是一个大算法,但是可以通过反串建SAM偷懒。
复杂度为
O
(
n
C
)
O(nC)
O(nC)。
解析
对反串建出后缀自动机,其
f
a
i
l
fail
fail 树即为所求的后缀树。
设
p
l
x
pl_x
plx,为
x
x
x 节点对应的任意一个出现位置,那么
f
a
i
l
x
→
x
fail_x\to x
failx→x 这条边上对应的字符串就是
s
(
p
l
x
+
l
e
n
f
a
i
l
x
,
p
l
x
+
l
e
n
x
−
1
)
s(pl_x+len_{fail_x},pl_x+len_x-1)
s(plx+lenfailx,plx+lenx−1)。
结合
f
a
i
l
fail
fail 的定义应该不难得到。
后缀树的性质:
- 根到叶子的路径和所有后缀一一对应。
- 所有非根节点都至少有两个子节点。
- L C P ( i , j ) LCP(i,j) LCP(i,j) 就是两个位置对应节点 lca 的最长长度。
在解决字典序相关问题时较为常用。