-1.参考资料
嗯,改了一些可能是错误的地方(迫真
0.定义
定义 (Sigma) 表示字符集。
定义 (asqsubseteq b) 表示 (a) 是 (b) 的后缀。
定义 (asqsubset b) 表示 (a) 是 (b) 的真后缀。
定义 (|s|) 表示 (s) 的长度。
定义 (u-v) 表示 (u) 到 (v) 的路径上节点组成的集合。
1.关于SAM
- Q:SAM 是什么?可以吃吗?
- A:SAM 全称后缀自动机(( exttt{Suffix Automation})),是一个接受 (s) 的所有后缀的最小 DFA ,不可以吃。
- Q:DFA 是什么?可以吃吗?
- A:DFA 全称确定有限状态自动机(( exttt{Deterministic Finite Automaton})),也不可以吃。DFA 由五个部分组成:
- 字符集((Sigma)),自动机中的所有字符均在字符集中。
- 状态集合((Q)),如果将 DFA 看成 DAG ,那么 (Q=V) 。
- 起始状态((s)),(sin Q),是一个特殊的状态。
- 接受状态((F)),(Fsubseteq Q),是一堆特殊的状态。
- 转移函数((delta)),如果将 DFA 看成 DAG ,那么 (delta=w) ,且 (delta:V imes V ightarrow Sigma),此处乘法表示笛卡尔积。
总结一下:
- SAM 是一个特殊的 DAG,结点称为状态,边称为转移。
- 存在源点(初始状态) (t_0),其他结点均由 (t_0) 可达。
- 存在多个汇点(终止状态),满足任意一条由源点到汇点的路径上转移函数拼接组成的字符串均为原字符串的后缀。(这也是其被称为后缀自动机的原因)
- 在所有满足以上条件的自动机中,SAM 是结点个数最少的。具体地,(|V|=2n-1,|E|=3n-4)。
由定义我们可以得到一个简单的推论:由源点引出的所有路径上转移函数拼接组成的字符串(包括连到自己的路径,这种情况下我们视为空串)构成的集合即为该字符串的所有子串构成的集合,证明显然。
2.SAM 举例
盗来了几张 oi-wiki 上的图:
- 空串:
- (s= exttt{a}):
- (s= exttt{aa}):
- (s= exttt{ab}):
- (s= exttt{abb}):
- (s= exttt{abbb}):
3.线性构造 SAM
Note:下面出现的一切字符串都是 (s) 的非空子串。
3.1 endpos
定义 (operatorname{endpos}(t)) 表示在字符串 (s) 中 (t) 的所有结束位置。
例如,当 (s= exttt{abcbc}) 的时候,(operatorname{endpos}( exttt{bc})={2,4})
我们称 (s,t) 为等价类,当且仅当 (operatorname{endpos}(s)=operatorname{endpos}(t)),于是我们可以将 (s) 的所有非空子串分为若干等价类。
则显然 SAM 上除了初始状态的每一个状态都对应一个等价类。
因为每个状态都能对应多个 (operatorname{endpos}) 相同的子串,也就是等价类,所以等价类的个数加上 1,就是SAM状态的个数!
下面我们会给出一些关于 ( ext{endpos}) 的性质。
Lemma 1:(|u|le |w|),那么 (u) 和 (w) 是等价类的充要条件是每次 (u) 出现都是作为 (w) 的后缀。
证明显然。
Lemma 2:(|u|le |w|),有:
Proof
若 (operatorname{endpos}(u)cap operatorname{endpos}(w) eqvarnothing),则 (u) 和 (w) 在相同位置结束,则 (usqsubseteq w),所以 (operatorname{endpos}(w)subseteq operatorname{endpos}(u)),反之亦然,得证。
Lemma 3:
(1)对于同一个等价类中的两个子串 (s,t),且 (|s|le |t|),有 (ssqsubseteq t)。
(2)一个等价类中的所有子串的长度,可以恰好不重复地覆盖一段连续区间。
Proof
如果等价类中只有一个子串,那么结论显然成立。
考虑有至少两个子串的情况,由 Lemma 1,短的子串是长的子串的后缀,所以等价类中子串长度两两不同。
设最长串为 (u),最短串为 (w),由 Lemma 1,(wsqsubset u)。
考虑长度在 ([|u|,|w|]) 之间的 (w) 的后缀,由 Lemma 1,这些字符串都在等价类中,所以(2)得证。
又因为等价类中子串长度两两不同,所以等价类中只有这些字符串,所以(1)得证。
3.2 link
取一个状态 (v eq t_0),我们目前已经得到了如下结论:
- (v) 对应一个等价类
- 如果设 (w) 是这个等价类中最长的一个,那么其他的字符串都是 (w) 的后缀。
- 如果将 (w) 的后缀按照长度降序排序,那么 (w) 的前几个后缀都在这个等价类中,且剩下的都不在。
我们设 (t) 是所有 (w) 的后缀中,最长的与 (w) 不在一个等价类中的后缀,那么定义 ( ext{link}(v)=t),即把 (v) 的后缀链接连到 (t) 上。
也就是说,一个后缀链接连接到对应于 (w) 的最长后缀的另一个等价类的状态。
下面给出一些性质。
P.S.为了方便起见,定义 (operatorname{endpos}(t_0)={-1,0,cdots,|S|-1})。
Lemma 4:所有后缀链接构成一棵根节点为 (t_0) 的树,一般称之为 Parent Tree。
Proof
证:(forall v eq t_0),由 Lemma 3,后缀链接连接的状态对应的字符串长度严格小于该状态字符串长度,因此必然会通向 (t_0),又因为恰有 (|V|-1) 条边,所以构成树。
Lemma 5:(operatorname{endpos}(s)subseteq operatorname{endpos}(operatorname{link}(s)))。亦即,parent tree的子节点的 endpos 是父节点的 endpos 的子集。
Proof
证:由定义 (operatorname{link}(s)sqsubseteq s) 由 Lemma 2 得证。
这是一张 parent tree 对应自动机的图:
3.3 一些定义&小结
在学习算法之前,先来一些定义。
- 定义 (operatorname{longest}(v)) 为状态 (v) 对应等价类中最长的字符串,(operatorname{len}(v)=|operatorname{longest}(v)|)。
- 定义 (operatorname{shortest}(v)) 为状态 (v) 对应等价类中最短的字符串,(operatorname{minlen}(v)=|operatorname{shortest}(v)|)。
所以由 Lemma 3 以及 link 的定义立得:
下面是以上定理的总结:
- (s) 的子串可以根据 endpos 划分为若干等价类。
- SAM 由初始状态和等价类对应的状态构成。
- 对于状态 (v eq t_0),有以下结论:
- 此状态对应的字符串均为 (operatorname{longest}(v)) 后缀,且长度恰好覆盖 ([operatorname{minlen}(v),operatorname{len}(v)])。
- (operatorname{endpos}(v)subseteq operatorname{endpos}(operatorname{link}(v)))
- parent tree 上 (t_0-v) 存在,且路径上每个节点对应的区间互不相交,并集恰为 ([0,operatorname{link}(v)])。
3.4 算法
这个算法是在线算法,也就是说可以每次加一个字符,同时更新SAM。
下面我们以字符串 ( exttt{abcbc}) 对应 SAM 为例,考虑加上一个 ( exttt b)(下面标记为字符 (c))。
下面的图分别表示 SAM 和 parent tree。
注:节点上的字符串表示 (operatorname{longest}(v)),边上的字符串表示转移函数的值,蓝色点是初始状态,绿色点是终止状态。实际算法时,我们只会对每个点记录转移到的点,(operatorname{len}) 和 (operatorname{link})。
在算法流程中,我们取 (last) 表示整个字符串对应的状态,每次更新完自动机后再更新 (last),比如在上面的图中,(last) 就是 ABCBC 对应的点,后面会标记为橙色。
- 新建状态 (cur),并令 (operatorname{len}(cur)=operatorname{len}(last)+1), (operatorname{longest}(v)=s),后面会用红色标记。
- 从 (last) 开始在 parent tree 上暴力跳祖先,只要跳到有到 (c) 的转移就停下,并将跳过的路径上的所有点
(包括 (last),不包括该点)全部向 (cur) 连转移为 (c) 的边,将此点设为 (p)。比如在这个例子中,找到的 (p) 就是 ( exttt{BC}) 对应的状态,用黄色标记,下面是操作好的自动机。
- 如果发现 (p) 不存在,那么令 (operatorname{link}(cur)=t_0),结束;否则,我们设它通过 (c) 转移到的状态为 (q),标记为青色。比如在上图中,( exttt{ABCB}) 对应的状态就是 (q)。
- 如果发现 (operatorname{len}(p)+1=operatorname{len}(q)),那么令 (operatorname{link}(cur)=q),结束。
- 否则我们新建一个状态 (clone),复制 (q) 的 (operatorname{link}) 以及转移的信息,并且令 (operatorname{len}(clone)=operatorname{len}(p)+1) ,将 (clone) 标记为紫色,下面是操作好的自动机和 parent tree。
- 然后我们令 (operatorname{link}(cur)=operatorname{link}(q)=clone),下面是操作好的 parent tree。
- 最后我们用 parent tree 从 (p) 往上跳,跳到的点只要对 (q) 有转移,就重定向到 (clone),下面是操作好的自动机。
注:易证这些有转移的点是连续的,实现时只要发现不能转移就终止。
8. 完成以上操作之后,我们将 (last) 的值更新为 (cur),并转化为初始自动机的形式,如图所示。
这里补充一下怎么找到终止状态的:parent tree 上从 (last) 节点往上跳,跳的时候经过的所有状态都是终止状态,因为这样可以遍历所有 (s) 的后缀。
下面是代码实现:
Code
struct node{int len,link,to[30];};
struct Suffix_Automation{
node state[N];
int rt=1,last=1,tot=1;
void extend(int ch){
int cur=++tot,p,q;
state[cur].len=state[last].len+1;
for(p=last;p&&!state[p].to[ch];p=state[p].link)state[p].to[ch]=cur;
if(!p)state[cur].link=rt;
else{
q=state[p].to[ch];
if(state[p].len+1==state[q].len)state[cur].link=q;
else{
int clone=++tot;
state[clone]=state[q];
state[clone].len=state[p].len+1;
state[cur].link=state[q].link=clone;
for(;p&&state[p].to[ch]==q;p=state[p].link)state[p].to[ch]=clone;
}
}
last=cur;
}
}SAM;
4.应用
咕咕咕~