您的位置:首页 > 其它

后缀自动机(SAM)学习笔记

2016-03-30 20:31 991 查看

构图及原理

定义

算法

后缀自动机(SAM)就是一个要实现能存下一个串中所有子串的算法,按一般来说应当有O(N2)O(N^2)个状态,而SAM却可以用O(N)个状态来表示所有子串,因为它把很多个本质相似的子串映射到了同一个状态上,从而实现了这个优美的算法。

SS:原串,我们要求的就是SS的后缀自动机。

RightstrRight_{str}:表示strstr在母串SS中所有出现的结束位置的集合。

ss(状态):表示所有RightRight集相同的字符串合成的状态。

ParentsParent_s:表示RightRight集包含了RightsRight_s并且集合最小的状态。设转移的表示为trans(状态,字符)=状态trans(状态,字符) = 状态

parentparent边:一个状态指向它的ParentsParent_s。(而由parentparent边构成的树可以称为parentparent树)

′a′'a' ~ ′z′'z'(或某些字符)字符边:有一个状态,后面加上一个字符后所指向的状态。

注意:

1. 一个状态可以由多条字符边转移而来,因为它包含多个子串,但只能有一条ParentParent边转移来。

2. 一个状态连出去的字符边不一定所有包含的子串后都能接这个字母,只是代表有某些包含的子串后能接这个字母。

一些简单的性质

1.RightRight集的性质

对于两个子串aa, bb的RightRight,只有两种情况,有交集和没有交集。考虑有交集的情况,如果有交集,那么显然一个子串是另一个子串的后缀,设aa是bb的后缀,那么Rightb⊂RightaRight_b\subset Right_a。即对于两个子串的RightRight要么包含,要么没有交集。所以对于一个状态ss所表示的字符串,它们的右端点必定相同,而左端点必定是连续的一段。如下图:



如果一个状态的RightRight集越大,就说明符合的子串越多,那么限制肯定就越小,所以左端点距右端点的距离就越小。而随着右端点的向右移动,符合一种条件的子串就越来越少,所以RightRight集就会变小(当然可能会分出两组不同的状态)。我们令一个状态ss所表示的区间是[Mins,Maxs][Min_s, Max_s],显然的Mins−1=MaxParentsMin_s - 1 = Max_{Parent_s},对于一个状态,我们记LensLen_s表示MaxsMax_s。

2.ParentParent树的性质

怎么理解ParentParent树?我们从叶子结点往根上走时,就是一些不相交的RightRight集不断合并的过程(因此我们不需要把RightRight集的全部节点存下来)。

如果trans(s1,c)=trans(s2,c)=trans(s3,c)....=ttrans(s1, c) = trans(s2, c) = trans(s3, c) .... = t,那么状态s1,s2,s3...s1, s2, s3 ...在ParentParent树上肯定是在一段连续的链上的。因为在这些子串的右端点加上一个字符cc后,它们的RightRight集又重新相等,证明它们本来就是包含且连续的关系。

ParentParent边简单来说,即表示不断寻找后缀的过程

构造方法

注:这里很多推理都是有上面的性质得来的,如有不理解的可以再回顾一下性质。

假设我们已经完成了前|S|−1|S| - 1个字符的插入(为TT串),现在插入第|S||S|个(cc)字符(为SS串)。

要想SAM继续保持它的功能,就要加入SS串的所有后缀。而SS串的所有后缀都可以由TT的后缀加一个字符转移而来。由于必定存在一个RightxRight_x仅为|T||T|的状态。并且根据ParentParent树的性质,它的祖先的RightRight集肯定也含有|T||T|,所以它们肯定构成了一条在ParentParent树中的链。所以我们只要沿着这条链就能找到所有TT的后缀。而我们讨论一下在ParentParent链上走的意义,即不断跳到当前字符串的后缀(当我们走完这条ParentParent链时就意味着|T|的所有后缀都遍历完了)

在沿着这条链走的时候会出现两种情况,假设到达的状态是xx。

(我们新加一个节点npnp,表示RightRight只含|S||S|的新状态。即lennp=|S|len_{np} = |S|)

trans(x,c)=Nulltrans(x, c) = Null:即当前这个状态没有沿cc转移的边,所以我们只要连一条为cc的字符边到npnp就处理完这种情况了。

trans(x,c)≠Nulltrans(x, c) \ne Null:假设第一个找到的节点为xx,转移到的节点为qq。那我们怎么把|S||S|这个位置加入RightqRight_q?我们发现,如果强行把|S||S|加入RightqRight_q中,可能会使qq节点出现矛盾,即lenqlen_q变小。我们来看一下下面这个例子:



如图,如果lenq=lenx+1len_q = len_x + 1那么就不会有这种问题,直接让Parentnp=qParent_{np} = q就可以了。但如果lenq>lenx+1len_q > len_x + 1,详细的说,就意味着有更多的子串共享qq这一个状态,如果|S||S|加进当前状态的RightRight集,可能会出现这个状态中的一些子串与到达|S||S|这个结束端点矛盾。如上面一些蓝色的串就不能放在后缀为|S||S|的位置,会与BB字符矛盾。那么讨论就要复杂点。我们可以把qq分成两种状态。如下图。



如图,我们可以把qq串分出一个nqnq来解决这个问题,只要我们把它再细分化,把符合结束位置为|S||S|的子串和不符合的分开。就可使nqnq的RightRight集包含|S||S|,从而解决这个问题。那么显然lennq=lenx+1len_{nq} = len_x + 1,由于nqnq是qq分出来的一个RightnqRight_{nq}包含RightxRight_x的状态,所以自然Parentq=nq,Parentnq=xParent_q = nq, Parent_{nq} = x,当然也要使Parentnp=nqParent_{np} = nq。并且考虑nqnq字符边的转移,由于结束位置为|S||S|的子串后是没有字符的所以它的转移状态是和qq一样的。

注意第一种情况一定是在第二种情况前出现的,而第二种情况就意味着一段段后缀的处理。

最后,我们还要处理别连向qq的状态,可以知道的是它们是连续一段的,所以把它们转移到qq的边都改为转移到nqnq的边就可以了。而对于其它有cc的字符边,且指向状态不为qq的,由于qq都已经能分离出合法状态了,那么那些节点的RightRight集都是可以直接加入|s||s|。至此,我们就已经把TT的所有后缀都建立了转移到SS后缀的方案。

程序

非常好实现。

//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后缀自动机》(这个讲的比较详细,容易理解)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: