构图及原理
定义
算法
后缀自动机(SAM)就是一个要实现能存下一个串中所有子串的算法,按一般来说应当有 O(N2) 个状态,而SAM却可以用O(N)个状态来表示所有子串,因为它把很多个本质相似的子串映射到了同一个状态上,从而实现了这个优美的算法。
点
-
S
:原串,我们要求的就是
S 的后缀自动机。 - Rightstr :表示 str 在母串 S 中所有出现的结束位置的集合。
s (状态):表示所有 Right 集相同的字符串合成的状态。- Parents :表示 Right 集包含了 Rights 并且集合最小的状态。设转移的表示为 trans(状态,字符)=状态
边
- parent 边:一个状态指向它的 Parents 。(而由 parent 边构成的树可以称为 parent 树)
- ′a′ ~ ′z′ (或某些字符)字符边:有一个状态,后面加上一个字符后所指向的状态。
注意:
1. 一个状态可以由多条字符边转移而来,因为它包含多个子串,但只能有一条
Parent
边转移来。
2. 一个状态连出去的字符边不一定所有包含的子串后都能接这个字母,只是代表有某些包含的子串后能接这个字母。
一些简单的性质
1. Right 集的性质
- 对于两个子串
a
,
b 的 Right ,只有两种情况,有交集和没有交集。考虑有交集的情况,如果有交集,那么显然一个子串是另一个子串的后缀,设 a 是b 的后缀,那么 Rightb⊂Righta 。即对于两个子串的 Right 要么包含,要么没有交集。所以对于一个状态 s 所表示的字符串,它们的右端点必定相同,而左端点必定是连续的一段。如下图:
- 如果一个状态的
Right 集越大,就说明符合的子串越多,那么限制肯定就越小,所以左端点距右端点的距离就越小。而随着右端点的向右移动,符合一种条件的子串就越来越少,所以 Right 集就会变小(当然可能会分出两组不同的状态)。我们令一个状态 s 所表示的区间是[Mins,Maxs] ,显然的 Mins−1=MaxParents ,对于一个状态,我们记 Lens 表示 Maxs 。
2. Parent 树的性质
- 怎么理解 Parent 树?我们从叶子结点往根上走时,就是一些不相交的 Right 集不断合并的过程(因此我们不需要把 Right 集的全部节点存下来)。
- 如果
trans(s1,c)=trans(s2,c)=trans(s3,c)....=t
,那么状态
s1,s2,s3...
在
Parent
树上肯定是在一段连续的链上的。因为在这些子串的右端点加上一个字符
c
后,它们的
Right 集又重新相等,证明它们本来就是包含且连续的关系。 - Parent 边简单来说,即表示不断寻找后缀的过程
构造方法
注:这里很多推理都是有上面的性质得来的,如有不理解的可以再回顾一下性质。
假设我们已经完成了前
|S|−1
个字符的插入(为
T
串),现在插入第
要想SAM继续保持它的功能,就要加入
S
串的所有后缀。而
在沿着这条链走的时候会出现两种情况,假设到达的状态是
x
。
(我们新加一个节点
trans(x,c)=Null :即当前这个状态没有沿 c 转移的边,所以我们只要连一条为
c 的字符边到 np 就处理完这种情况了。trans(x,c)≠Null :假设第一个找到的节点为 x ,转移到的节点为
q 。那我们怎么把 |S| 这个位置加入 Rightq ?我们发现,如果强行把 |S| 加入 Rightq 中,可能会使 q 节点出现矛盾,即lenq 变小。我们来看一下下面这个例子:
如图,如果 lenq=lenx+1 那么就不会有这种问题,直接让 Parentnp=q 就可以了。但如果 lenq>lenx+1 ,详细的说,就意味着有更多的子串共享 q 这一个状态,如果|S| 加进当前状态的 Right 集,可能会出现这个状态中的一些子串与到达 |S| 这个结束端点矛盾。如上面一些蓝色的串就不能放在后缀为 |S| 的位置,会与 B 字符矛盾。那么讨论就要复杂点。我们可以把q 分成两种状态。如下图。
如图,我们可以把 q 串分出一个nq 来解决这个问题,只要我们把它再细分化,把符合结束位置为 |S| 的子串和不符合的分开。就可使 nq 的 Right 集包含 |S| ,从而解决这个问题。那么显然 lennq=lenx+1 ,由于 nq 是 q 分出来的一个Rightnq 包含 Rightx 的状态,所以自然 Parentnq=Parentq,Parentq=nq ,当然也要使 Parentnp=nq 。并且考虑 nq 字符边的转移,由于结束位置为 |S| 的子串后是没有字符的所以它的转移状态是和 q 一样的。
注意第一种情况一定是在第二种情况前出现的,而第二种情况就意味着一段段后缀的处理。
最后,我们还要处理别连向
程序
非常好实现。
//Suffix Automaton’s Build YxuanwKeith
//S为根,一开始tot = 1表示已经给根编号为1
void Add(int c) {
int Nt = ++ tot, p = Last;
//Last表示前缀T的最长后缀对应的状态。
Tr[Nt].Len = Tr[Last].Len + 1, Last = Nt;
for (; p && !Tr[p].Go[c]; p = Tr[p].Pre) Tr[p].Go[c] = Nt;
if (!p) Tr[Nt].Pre = S; else {
int q = Tr[p].Go[c];
if (Tr[p].Len + 1 == Tr[q].Len) Tr[Nt].Pre = q; else {
int Nq = ++ tot;
Tr[Nq] = Tr[q];
Tr[q].Pre = Tr[Nt].Pre = Nq;
Tr[Nq].Len = Tr[p].Len + 1;
for (; p && Tr[p].Go[c] == q; p = Tr[p].Pre) Tr[p].Go[c] = Nq;
}
}
}
例题及应用(未完成)
(未完待续……)
参考资料
- 张天扬《后缀自动机及其应用》(这个可以作为辅助资料)
- 陈立杰《Suffix Automaton后缀自动机》(这个讲的比较详细,容易理解)