您的位置:首页 > 其它

后缀自动机SAM

2017-07-05 19:30 363 查看
ps:陈立杰(Orz%%%)的课件大家可以去看一下,很有帮助,我加了一些自己的理解并解释的更清楚一些(然而好像并没有特别清楚QAQ)

再ps:水平极其有限,有些地方并没有深究,如果有错误或者需要增进的地方,欢迎指出!

作用

后缀自动机(Suffix Automaton,简称SAM)能够识别一个字符串的所有后缀(但是在之后我们会发现实际上能够识别所有字串),可以干很多后缀数组以及后缀树能干的事情。而且后缀自动机的构造复杂度是线性的,代码也非常简短。

自动机

先简要说说自动机是什么(学AC自动机的时候我竟然并不知道自动机是个什么东西:P)。自动机可以识别字符串S等价于从初始状态沿着S走能够走到结束状态,自动机可以从某状态s开始识别字符串S等价于从s沿着S走能够走到结束状态。

性质

ps:写的又渣又长,背板者如果想可以直接看模板

实际上能够识别一个字符串所有后缀的自动机有很多很多,比如这个(略去结束状态):



这货实际上就是后缀全部插入,节点数是O(n2)的,太多了啊,所以我们需要求出的后缀自动机实际上是最简状态后缀自动机(状态数最少的后缀自动机),而不是其他不是最简的后缀自动机。

我们用ST(S)表示从初始状态沿着S走到达的状态。如果S的一个字串A出现在了S的[l,r),那么ST(A)就可以识别S从r开始的后缀(记为Suffix(r)),所以假设A在S中出现的位置为[l1,r1),[l2,r2),…,[ln,rn),则ST(A)就能够识别Suffix(r1),Suffix(r2),…,Suffix(rn)。我们把{r1,r2,…,rn}记录为Right(A)表示A的右端点集合。由此可以发现如果两个字串A,B满足Right(A)=Right(B),ST(A)应该要等于ST(B),这样才能够减少状态数(ST(A)和ST(B)都能识别相同的一些后缀,莫不如将状态合并为一个),所以我们再定义Right(s)表示s这个状态的右端点集合。

根据Right的定义,我们可以倒过来定义一个子串:已知Right(s)和合适的长度len,就对应了一个子串。但为什么是合适的长度len呢?因为如果len过大,对应的子串的右端点集合就比Right(s)少了,反之如果len过小,对应的子串的右端点集合就比Right(s)多了。所以len的取值范围一定是个区间,我们将其设为[MIN(s),MAX(s)]。

我们会发现一个性质,就是任何两个不同的状态ST(A)和ST(B)(不妨假设length(A)<=length(B))之间,Right(ST(A)),Right(ST(B))要么没有交集,要么Right(ST(B))是Right(ST(A))的真子集。这个性质其实很容易得出,如果A是B的后缀,那么由于A比B短,所以Right(ST(A))的元素个数肯定大于Right(ST(B)),如果A不是B的后缀,那么A就不可能有右端点和B相同,否则因为A比B短,A将会成为B的后缀。

由于Right之间要么没有交集,要么是另一个的真子集,所以所有状态实际上可以构成一棵树(注:这和后缀自动机的形态并没有什么关系,我们称其为father树)。而father树的叶节点个数为n(S的所有后缀代表的状态),且除叶节点之外的所有节点都至少有两个儿子,所以个数是O(n)的(可以联想一下线段树节点个数为2*n-1)。我们假设father树中状态s的父亲状态是father(s),那么我们可以发现当合适的长度len不停减小小于MIN(s)之后,对应的子串的右端点集合其实就变成了Right(father(s)),所以MAX(father(s))=MIN(s)-1。

但是只是证明状态个数是线性的并不能证明后缀自动机是线性的,还需要证明边数是线性的才行。

后缀自动机并不是一棵树,但是我们可以从中选出一棵生成树,那么生成树的边数是O(n)的,我们主要需要计算出非生成树边的个数。由于后缀自动机可以识别所有后缀,所以从初始状态->非生成树边->结束状态肯定对应了一个后缀。我们把一个后缀与其向上走遇到的第一条非生成树边相对应,那么一个后缀至多对应一条非生成树边,而一条非生成树边至少被一个后缀对应。所以非生成树的数量不会多于后缀数量,所以也是O(n)的。

这么一来我们就证明了一系列性质和最重要的线性性质,来看构造吧!

构造

ps:写的又渣又长,背板者如果想可以直接看模板

我们假设已经求好了T的后缀自动机(最简状态,下同),现在需要添加字符w,使其变为Tw的后缀自动机。那么实际上我们增加了一些是Tw后缀的子串。

由于原来T是最长的后缀,所以必定存在Right(p)={length(T)}的一个状态p,同理在插入w后必定存在Right(np)={length(T)+1}的一个状态np。那么代表T后缀的状态v1,v2,v3,…,vk在father树中肯定都是p的祖先(所以Right集合中一定都含有length(T))。

不妨把v按照深度由大到小排个序:v1=p,v2,v3,…,vk=root。假设一个v的Right为{r1,r2,r3,…,rn=length(T)},那么只有S[ri]=w的ri是符合要求的。所以如果v状态没有编号为w的边,就说明v的Right中没有任何一个ri(除rn)是满足要求的,这样的话唯一满足要求的就是rn了,直接将v与np连一条编号为w的边即可。但是如果v状态有编号为w的边(这意味着v的祖先都有),就需要继续考虑下去了。

假设vp是第一个有编号为w的边的状态,Right(vp)={r1,r2,r3,…,rn}。根据前面说到的,肯定有ri满足S[ri]=w的要求,假设q是vp沿着编号为w的边走的状态,则Right(q)={ri+1|S[ri]=w}(注:因为q是没有插入之前w的vp的下一状态,所以Right(q)中并没有rn+1)。然后由于插入w我们需要在q的Right中加入length(T)+1,结果我们会发现这并不一定可行!如图:

红色的是长度为MAX(vp)的子串,蓝色的是长度为MAX(q)的子串。



如果强行加入了length(T),那么MAX(q)就可能会变小,这就导致性质出问题。所以我们通常并不能在Right(q)中加入length(T),但如果MAX(vp)+1=MAX(q)就可以直接加入了,如图:



接下来我们看不能加入length(T)怎么办,由于不能改变原性质,我们肯定需要新建节点了。新建节点nq,使nq=Right(q)∪{length(T)+1},那么就会发现MAX(nq)=MAX(vp)+1(从第一张图中可以比较直观的看出,然后再yy一下~)。由于Right(nq)=Right(q)∪{length(T)+1},所以father(q)=nq。然后我们再思考会发现father(np)=nq,且father(nq)=原来的father(q)。下面简单证明一下后者:

因为father(q)是q的父亲,所以Right(q)⊂Right(father(q))。我们需要注意到,Right(father(q))在添加w过后其实多了{length(T)+1}(只不过并没有在操作中反应出来,因为不需要维护Right),又因为Right(q)⊂Right(father(q)),所以Right(nq)⊂Right(father(q)),即father(nq)=原来的father(q)。

但q原来的转移怎么办?因为nq虽然多了length(T)+1,但length(T)+1后面并没有东西,不影响转移,所以直接把q的转移copy给nq就行啦!同时,除了vp,其他原先连向q的v(即vp的部分祖先,这部分祖先是连续的,因为随着Right集合的增大,从一个祖先开始编号为w的边就不会连向q了)也需要把编号为w的边连向nq。至此就搞完啦!

由于我们只有在迫不得已的时候才加新状态,所以构造出来的后缀自动机是最简状态的(并不会严格证明啊QAQ)。同时由于构造的时候都会往上跳,最后才往下跳一次,所以构造的效率是O(n)的!如果对效率是线性的没有什么直观感受,可以看下模板。

模板

说了这么多这么多,终于到了模板,其实代码非常简短,我们梳理一下构造:

1.上一次Right(p)={length(T)}的状态p。

2.新建状态np,使Right(np)={length(T)+1}。

3.找到第一个有编号为w的边的p的祖先vp,没找到之前的p的祖先都连一条编号为w的边到np。

4.如果没有vp,那么father(np)=root。否则令q是vp沿着编号为w的边走的状态。

5.如果MAX(vp)+1=MAX(q),那么father(np)=q。否则新建节点nq,将q的转移copy给nq,且father(nq)=father(q),father(q)=nq,father(np)=nq,并将所有原先连向q的v的编号为w的边连向nq。

注:只需要维护father和MAX,并不需要维护Right。

struct SAM
{
struct node
{
node *son[maxi],*fa;int MAX;
node(int M) {MAX=M;fa=0;memset(son,0,sizeof(son));}
};
typedef node* P_node;
P_node ro,lst; //ro即root,lst记录上一次的p
SAM() {ro=new node(0);lst=ro;}
void Insert(char ch) //在原先的基础上插入ch
{
int ID=ch-'a';P_node p=lst,np=new node(lst->MAX+1);
while (p&&!p->son[ID]) p->son[ID]=np,p=p->fa; //找vp
if (!p) np->fa=ro; else //找不到
{
P_node q=p->son[ID];
if (p->MAX+1==q->MAX) np->fa=q; else //直接转移
{
P_node nq=new node(p->MAX+1);
memcpy(nq->son,q->son,sizeof(q->son));
nq->fa=q->fa;q->fa=nq;np->fa=nq;
while (p&&p->son[ID]==q) p->son[ID]=nq,p=p->fa;
//新建节点nq,并进行一系列修正
}
}
lst=np; //重新记录lst
}
void make_SAM(char *s) {for (int i=1;s[i];i++) Insert(s[i]);}
};


题目(施工中)

SPOJ1811,求两个字符串的最长公共子串,题解传送门

SPOJ1812,求n个字符串的最长公共子串,题解传送门

HDU4622,求一个字符串中不同子串的个数,题解传送门
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: