[后缀自动机]【学习笔记】
SAM ..................Smith ?
参考资料:
1.陈立杰课件
3.https://huntzhan.org/suffix-automaton-tutorial/
4.http://codeforces.com/blog/entry/20861
说明:
花了晚上两个小时+一上午(估计还要一下午写笔记).....我主要看了clj的课件,算法过程主要依照课件上,其他两篇文章作为辅助补充一些证明和性质,课件中有点错误让我很纠结....
那些资料是非常好啦,然后我就乱写一点加深一下印象
前置技能:
有限状态自动机的功能是识别字符串,令一个自动机A,若它能识别字符串S,就记为A(S)=True,否则A(S)=False。
自动机由五个部分组成,alpha:字符集,state:状态集合,init:初始状态,end:结束状态集合,trans:状态转移函数。
不妨令trans(s,ch)表示当前状态是s,在读入字符ch之后,所到达的状态。
如果trans(s,ch)这个转移不存在,为了方便,不妨设其为null,同时null只能转移到null。
null表示不存在的状态。
同时令trans(s,str)表示当前状态是s,在读入字符串str之后,所到达的状态。
$Suffix\ Automaton$
-------$Direcged\ Acyclic\ Word\ Graph\ (DAWG)$
Defination:
A suffix automaton A for a string s is a minimal finite automaton that recognizes the suffixes of s. This means that A is a directed acyclic graph with an initial node i, a set of terminal nodes, and the arrows between nodes are labeled by letters of s. To test whether w is a suffix of s it is enough to start from the initial node of i and follow the arrows corresponding to the letters of w. If we are able to do this for every letter of wand end up in a terminal node, w is a suffix of s. From:http://codeforces.com/blog/entry/20861
注意定义中的最简状态
Concept & Property:
$ST(str)$表示$trans(init,str)$
$Reg(s)$表示从状态$s$开始能识别的$string$,可以发现这是一些$suffix$的集合
(因为能识别str表明当前+str组成了一个suffix,那么str也是一个suffix)
对于string a
$Reg(ST(a))={suf(r_1),suf(r_2),...,suf(r_n)}$
$Right:$
$Right(a)={r_1,r_2,...,r_n}$
一个状态的$Reg$由$Right$集合来决定
SAM中的一个状态s表示了一个$Right-equivalence\ class$,也就是所有$Right集合=Right(s)$的子串
有了Right只需要一个长度就可以确定子串
对于$Right(s)$,适合他的子串的长度在一个范围内,记作$[Min(s),Max(s)]$
对于任意两个状态$a,b$,$Right(a)$和$Right(b)$如果相交,设$Max(a)<Min(b)$,那么$Right(b)$是$Right(a)$的真子集
//因为a是b的后缀啊,这里课件上写反了QAQ//
否则不相交
Right集合的包含关系形成的树形结构叫做$Parent\ Tree$
//其他文献中叫做$Suffix\ Link$ 这个边是从孩子指向父亲的,和Fail Tree有点像//
Parent树从上往下Right集合变小,子串长度变长
$fa=Parent(s)\ \rightarrow\ Right(s) \subset Right(fa)$且$Right(fa)$最小
发现$Max(fa)=Min(s)-1$
//Min(s)-1就多于Right(s)了,就到了Right(fa)//
Parent Tree的叶子节点数O(N),每个内部节点至少两个孩子,所以总结点数O(N)
//等比数列求和啊//
补充一点:
$Max(s)$也表示了SAM上root到s最多走几步,从root到s的所有路径范围就是$[Min(s),Max(s)]$,因为一条路径就是一个能转移到s状态的子串啊
Suffix Link有点像失配吧,当前状态s走不了了就到Suffix Link指向的状态fa上去,fa是s的后缀所以是可行的,并且有更多走的机会
其他性质:
1.可以证明,在SAM中节点数不超过$2n-2$,边数不超过$3n-3$(包括转移边和Parent Tree的边)
2.Suffix Link从父亲指向儿子后就是Reverse(s)的Suffix Tree 反向字符串的后缀树!后缀树是一颗经过压缩的字典树
//随便证一下:一个节点Right等价,一段后缀相同(相当于压缩),然后Suffix Link连的点Right集合不同,也就是有不同的边出去了,所以不能压缩//
3.s有--c--> Parent(s)也有 并且人家Right还大
4.s--c-->t Max(t)>Max(s)
5.两个串的最长公共后缀,位于这两个串对应状态在Parent树上的最近公共祖先状态
子串的性质:
从init开始走转移边可以得到所有子串 每个子串都必然包含在SAM的某个状态里
一个状态的Right集合就是他的子树(叶子)的Right的并集
$SAM\ Online\ Construction:$
去看课件吧很详细啦......
使用了Parent Tree的性质,保证了空间O(N),时间也是O(N),证明不管啦
简单一说:
- 当前T,长度L,加入x
- 令p=ST(T) ,则Right(p)={L}
- 新建np=ST(Tx)
- p的Parent祖先的Right里都有L
- 对于没有--x-->的祖先v,trans(v,x)=np
- IF 一直到root之后也没有这样的祖先,Parent(np)=root
- ELSE p=第一个有的祖先,令q=trans(p,x) //注意这里的Right(q)={ri+1|s[ri]==c}不包括rn
- IF Max(q)==Max(p)+1,说明强行加入L+1不会使Max(q)变小,直接Parent(np)=q
- ELSE 新建nq复制q,用nq代替trans(v,x)=q的q, Parent(nq)=Parent(q),Parent(q,np)=nq
会使变小:Right(q)的Max可能更长一点,最后加上x还是没他长
Code:
实现上:
1.last保存当前的ST(T) , val保存MAX(s) 也就是到root的最远距离
2.和写过的其他数据结构不一样,root和last要新开节点,因为即使走到root还是可以走的,Parent(root)=0
3.好短啊 感觉比SA还好写
4.注意iniSAM和走之前u=root
int c[N],a[N]; int s[N]; struct State{ int ch[26],par,val; }t[N]; int sz,root,last; inline int nw(int _){t[++sz].val=_;return sz;} inline void iniSAM(){sz=0;root=last=nw(0);} void extend(int c){ int p=last,np=nw(t[p].val+1); for(;p&&!t[p].ch[c];p=t[p].par) t[p].ch[c]=np; if(!p) t[np].par=root; else{ int q=t[p].ch[c]; if(t[q].val==t[p].val+1) t[np].par=q; else{ int nq=nw(t[p].val+1); memcpy(t[nq].ch,t[q].ch,sizeof(t[q].ch)); t[nq].par=t[q].par; t[q].par=t[np].par=nq; for(;p&&t[p].ch[c]==q;p=t[p].par) t[p].ch[c]=nq; } } last=np; } void RadixSort(){ for(int i=0;i<=n;i++) c[i]=0; for(int i=1;i<=sz;i++) c[t[i].val]++; for(int i=1;i<=n;i++) c[i]+=c[i-1]; for(int i=sz;i>=1;i--) a[c[t[i].val]--]=i; } SAM
struct node{ int ch[26],par,val; }t[N]; int sz=1,root=1,last=1; void extend(int c){ int p=last,np=++sz; t[np].val=t[p].val+1; for(;p&&!t[p].ch[c];p=t[p].par) t[p].ch[c]=np; if(!p) t[np].par=root; else{ int q=t[p].ch[c]; if(t[q].val==t[p].val+1) t[np].par=q; else{ int nq=++sz; t[nq]=t[q];t[nq].val=t[p].val+1; t[q].par=t[np].par=nq; for(;p&&t[p].ch[c]==q;p=t[p].par) t[p].ch[c]=nq; } } last=np; }
$Summary$
一定要时刻把握这几条性质:
1.走 子串
2.Parent Tree的祖先 Right集合变大,字符串变短(路径长度变短),并且是后代的后缀哦
3.出现次数向父亲(Parent边)传递,接收串数从儿子(仍然Parent边)获取
4.拓扑排序/对val用基数排序 , 然后可以转移边/Parent边 DP ,可以倒着递推出|Right|
5.