算法详解——后缀自动机 - Canopus_wym
算法详解——后缀自动机
DFA
DFA,即确定性有限状态自动机,由一个五元组M=(Σ,Q,qs,F,tr)M=(\Sigma,Q,q_s,F,tr)组成,其中:
Σ\Sigma为一个有限字符集,其中每个字符cc称为一个输入符号;
QQ为一个有限状态集合;
qs∈Qq_s\in Q为初始状态;
F⊆QF\subseteq Q称为终结状态集合;
tr∈Q×Σ→Qtr\in Q\times \Sigma\to Q称为状态转换函数。
tr(q,c)=q′tr(q,c)=q'表示当前状态为qq,输入符号为cc时,自动机MM将自动转换成下一个状态q′q',此时称q′q'为qq的一个后继状态。
以上全是废话。
大家应该都知道AC自动机吧,如果不知道可以去看这一篇博客:http://blog.csdn.net/wang3312362136/article/details/78659403
一个DFA,就是一个有向图,有一个起点,有一些标有字符的边,有一个或多个终点,从这个起点,按照一个字符串上的字符,按顺序走与字符串的字符相同的边,最终走到了终点,就说明这个DFA能够识别这个字符;如果走到了非法状态qϕq_\phi,或者走到了一个不是终点的状态,那么说明这个DFA不能识别这个字符串。
对于一个状态pp和qq,如果任何一个能够从pp转换到终点的字符串都可以从qq转换到终点,反之也成立,那么就说明pp和qq是等价状态,记为KaTeX parse error: Unexpected character: \’\’ at position 2: p̲~q。
如果一个DFA中没有等价状态,那么这个自动机叫做最小状态自动机。
SAM
即后缀自动机,是能识别一个串所有后缀的最小状态自动机。
SAM的性质
它有哪些性质呢?
Theorem 1
对于任意一个T∈Σ∗T\in \Sigma^*,tr(qs,T)̸=qϕtr(q_s,T)\not=q_\phi当且仅当TT是SS的一个字串。
Lemma 2
设T∈Σ∗T\in \Sigma^*是一个非空字符串,right(T)right(T)表示TT在SS中所有结束位置的集合,则有L(tr(qs,T))={sufr+1∣r∈right(T)}L(tr(q_s,T))=\{suf_{r+1}\mid r\in right(T)\}。
Theorem 3
设T1T_1,T2T_2是SS的两个非空字符串,则tr(qs,T1)=tr(qs,T2)tr(q_s,T_1)=tr(q_s,T_2)的充要条件是right(T1)=right(T2)right(T_1)=right(T_2)。
Theorem 3.5
记一个状态qq的rightright集合为RqR_q,则rightright集合恰好为RqR_q的串,其长度一定在一个区间内,称之为qq的合法长度区间,记为[minlq,maxlq][minl_q,maxl_q],对应从初始状态到这个状态的最短长度和最长长度。
Lemma 4
任取两个不同的状态p,qp,q,则下列三式中必有一式成立:
(1)Rp⋂Rq=ϕR_p\bigcap R_q=\phi (2)Rp⊆RqR_p\subseteq R_q (3)Rq⊆RpR_q\subseteq R_p
Lemma 5
若一个状态pp是状态pp的parentparent状态,当且仅当RqR_q是最小真包含RpR_p的集合,那么q∈Q {qϕ}q\in Q\text{\ }\{q_\phi\}的parentparent状态存在且唯一。
Theorem 6
设q∈Q {qϕ}q\in Q\text{\ }\{q_\phi\},pp是qq的parentparent状态,则有maxlp=minlq−1maxl_p=minl_q-1,此时pp对应的所有字串都是qq的后缀。
所以,我们在后缀自动机中只需要存储每个节点parentparent状态,和maxlmaxl值即可。
Theorem 7
对串SS构建后缀自动机MM,则有MM的状态数∣Q∣≤2∣S∣+1|Q|\leq 2|S|+1。
Theorem 8
对串SS构建后缀自动机MM,则合法状态转换边的数量不超过3∣S∣3|S|。
- 后缀自动机中只需存储SS的字串对应的状态;
- 一个状态对应的字串,它们的rightright集合相等,且长度取值必定对应一个区间;
- 每个状态与parentparent状态的关系必定构成一棵parentparent树;
- 一个状态的最小合法长度恰好比parentparent状态的最大合法长度多11;
- 后缀自动机是一个线性结构。
证明?
由自己的感觉可得
自己参阅资料吧。(其实可以背结论的)
SAM的构建
增量法,每次在后缀自动机中添加一个字符,然后对当前的SAM进行更新。
显然,添加一个字符并不会造成状态的合并,但有可能造成状态的分离,而分离的状态只有可能是在添加前后缀状态和非后缀状态都可以转换到它,此时,这个状态需要分裂成只能被后缀状态转换的状态和只能被非后缀状态转换的状态。(这个情况之后再讲)
设添加前这个串是SS,添加的字符为cc,设tr(qs,S)=ptr(q_s,S)=p,由于所有后缀节点在parentparent树上都是祖先关系,因此把它们记为v1=p,v2,v3,⋯ ,vk=qsv_1=p,v_2,v_3,\cdots ,v_k=q_s。
由于tr(qs,Sc)=qϕtr(q_s,Sc)=q_\phi,那么我们需要新建一个节点np=tr(qs,Sc)np=tr(q_s,Sc)(这个是显然的)
那么maxlnp=∣S∣+1maxl_{np}=|S|+1,Rnp′={n+1}R'_{np}=\{n+1\}。
对于一个后缀状态vv,如果tr(v,c)=qϕtr(v,c)=q_\phi(即vv没有符号为cc的合法转换边),那么将tr(v,c)=nptr(v,c)=np即可,正确性显然。
设vpv_p是v1,v2,v3,⋯ ,vkv_1,v_2,v_3,\cdots,v_k中第一个存在符号为cc的合法转换边的状态,那么记qq为tr(vp,c)tr(v_p,c),取r=Rqr=R_q中的最小值,此时记Q1=S[r,r],Q2=S[r−1,r],⋯ ,Qr=S[1,r]Q_1=S[r,r],Q_2=S[r-1,r],\cdots,Q_r=S[1,r],下标就是串的长度。
由于S[r]S[r]显然为cc,那么记Q1=P0c,Q2=P1c,⋯ ,Qr=Pr−1cQ_1=P_0c,Q_2=P_1c,\cdots,Q_r=P_{r-1}c,显然,Pi=S[r−i,r−1]P_i=S[r-i,r-1]。那么状态qq对应的串就是Qminlq,⋯ ,QmaxlqQ_{minl_q},\cdots,Q_{maxl_q},所有能转换到qq的状态pp对应的串就是SQ={Pminlq−1,⋯ ,Pmaxlq−1}S_Q=\{P_{minl_{q}-1},\cdots,P_{maxl_q-1}\}。
又由于vpv_p对应的后缀串只有SP={P0,⋯ ,Pmaxlvp}S_P=\{P_{0},\cdots,P_{maxl_{v_p}}\};
讨论qq的状态:
- 当maxlvp=maxlq−1maxl_{v_p}=maxl_q-1时,显然有SQ⊆SPS_Q\subseteq S_P,那么,所有能通过输入符号cc转换到qq的状态都是后缀状态,此时qq不会发生状态的分裂。而对于vpv_p在parentparent树上的祖先,显然tr(vp,c)tr(v_p,c)更不可能发生分裂。
- 当maxlvp<maxlq−1maxl_{v_p}<maxl_q-1时,qq的合法长度区间可以分成两个部分:[minlq,maxlvp+1][minl_q,maxl_{v_p}+1]以及[maxlvp+2,maxlq][maxl_{v_p}+2,maxl_q]。前者只有后缀状态能转移到它,后者只有非后缀状态能转移到它。
那么新建一个状态nqnq,对应前者,新图中的q′q'对应后者。由于RnqR_{nq}显然只比RqR_{q}多一个∣S∣+1|S|+1,而∣S∣+1|S|+1之后并没有字符,没有状态能通过这个转移,那么nqnq的状态转换边与qq完全相同。
而对于vpv_p在parentparent树上的祖先,显然,如果tr(vp,c)=qtr(v_p,c)=q则指向nqnq,否则不变。
对于npnp的parentparent:当v[1,n]v_{[1,n]}都不存在cc的转换边,说明npnp是第一次出现,parentparent是qsq_s。否则,npnp的parentparent是qq或者nqnq,取决于是否建立了nqnq这个状态。
对于q′q'和nqnq的parentparent:如果没有发生分裂,则不改变。如果发生了分裂,设原图中qq的parentparent为parqpar_q,则parqpar_q,qq和nqnq这三个状态在parentparent树上构成祖孙关系,进一步推理得parnq=parq,parq′=nqpar_nq=par_q,par_{q'}=nq。
代码
struct samnode
{
samnode* tr[26];
samnode* par;
int maxl;
int init(int l=0)
{
memset(tr,0,sizeof tr);
par=NULL;
maxl=l;
return 0;
}
};
struct suffix_automaton
{
samnode* qs;
samnode* qlast;
samnode node[maxn*3];
int cntnode;
inline int clear()
{
delete qs;
delete qlast;
qs=new samnode;
qlast=new samnode;
qs->init();
qlast=qs;
cntnode=0;
return 0;
}
inline int addchr(int ch)
{
samnode* p=qlast;
samnode* np=&node[++cntnode];
np->init(p->maxl+1);
qlast=np;
while((p!=NULL)&&(p->tr[ch]==NULL))
{
p->tr[ch]=np;
p=p->par;
}
if(p==NULL)
{
np->par=qs;
return 0;
}
samnode* q=p->tr[ch];
if(p->maxl+1!=q->maxl)
{
samnode* nq=&node[++cntnode];
nq->init(p->maxl+1);
memcpy(nq->tr,q->tr,sizeof q->tr);
nq->par=q->par;
q->par=nq;
np->par=nq;
while((p!=NULL)&&(p->tr[ch]==q))
{
p->tr[ch]=nq;
p=p->par;
}
}
else
{
np->par=q;
}
return 0;
}
};