构图及原理
定义
算法
后缀自动机(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后缀自动机》(这个讲的比较详细,容易理解)