后綴自動機入門詳解及模板
標簽: 后綴自動機
后綴自動機
自動機
要想了解后綴自動機,首先得了解自動機。
例如AC自動機,AC自動機可以識別一個字符串為其所匹配的前綴。
而我們今天所介紹的后綴自動機則是識別一個字符串為自動機串的子串。
在接下來的描述中為了方便,簡稱\(SAM\)。
暴力實現
我們知道字典樹有着優良的時空復雜度,並且可以支持識別一個字符串的前綴。
如果我們將串中的所有后綴插入進字典樹,那么就可以實現這個自動機的功能。
不過,由於忽視了后綴的這個性質,總點數高達\(O(n^2)\)。
即使如此,字典樹的存儲方式還是有一個優點:減少了不必要的重復存儲。
優化狀態
我們先設\(ST(a)\)代表從初始節點轉移字符串a所到的狀態。
考慮如何設置狀態來減少狀態的數量。
首先,對於相同的子串,我們沒有必要浪費空間。
字典樹是把前綴都放在一起存,那么我們是不是可以另辟蹊徑,把后綴都存在一起呢?
一個子串可以表示成一個后綴的前綴,或是前綴的后綴,我們暴力實現就是利用了前者的這個特點。
如果我們把其看做一個前綴的后綴,會不會有更加方便的存法呢?
我們現在狀態記錄的是一個后綴集合。
對於一個字符串s,我們設\(R(s)\)為\(s\)在\(S\)的位置集合的右端點 $ {r_1,r_2,r_3......,r_k} \( 如果對於兩個串,\)R(s)=R(t)$,那么這兩個串我們就存在一起,十分方便。
乍一看復雜度還是很鬼,如果直接這么存的話。
因為我們還是記錄下了每一個不等價的串。
接下來給出一個定理,可以優化掉大部分的狀態。
定理一:對於長度分別為\(l,r\)串\(s\),\(t\),,如果\(R(s)=R(t)\),那么對於\(l<=x<=r\)的長度為\(x\)的串,總有|R(s)|個滿足他們的R集合等於\(R(s)\).
這個結論很顯然,但是也很有用。
它告訴了我們,一個狀態真正有用的是\(R(s)\)集合對應了一個長度區間\([l,r]\)。這兩者可以共同表示這個后綴集合。
在長度區間中的任意長度的后綴,他們的\(R(s)\)集合都是相同的。
接下來我們證明,狀態數一定是O(n)個的。
形象的理解的話,首先\(R(\varnothing)=\{1,.....n\}\)。
接下來我們任意加入一些字符(屬於集合S),可以把\(R(\varnothing)\)分成若干個不同的集合\(R(a),R(b),R(c).......\)
對於這些集合,我們再加入字符(當然得到的那個字符串必須是\(S\)的子串)。
當然有時候加入字符並不會使得\(R\)變小,但是這樣的同時也不會使得狀態數增加。
但是一旦使其變小,就會分裂成多個狀態或是使某些元素消失。
分裂最多只會進行n-1次,而消失會進行n次。
所以狀態數是\(O(n)\)的。
可以看見,上面的情況是由於一個定理。
定理二:兩個\(R\)集合兩兩不相交或一個為一個的真子集。
接下來我們來證明這個定理。
首先兩個集合肯定不相等,然后如果兩個R集合相交,那么一個肯定為另一個的后綴,那么這就是真子集了。
從形象的角度理解,貌似這些狀態構成了一個樹狀的東西,我們稱之為parent樹。
如果對於一個狀態x,他的\(R _ x\)對應的區間為\([l_x,r _x]\)。
那么肯定可以找到一個狀態y,他的\(R _y\)對應的區間為\([l _y,r _y]\),並且\(r _y=l _x-1\)
那么我們令\(x.parent=y\).並且y所對應的后綴集合一定是x的后綴集合中的后綴。
在讓我們形象的來理解parent樹。
假如一個狀態x,\(R _x\)對應的區間\([l _x,r _x]\)中僅有\(R _x=\{r _x\}\)
那么我們可以想象出一個長度為\(r _x\)的串s,這個串s一開始是S的前綴,我們不斷地去掉s的前端,我們發現減到長度為\(l _x-1\)時,這個串竟然在前面出現過,那么集合\(R\)是不是會變化,那么對應了一個新的狀態.一直這樣下去,我們最后會減到\(\varnothing\),也就是空串。
是不是對parent樹有了新的理解?
再介紹一個東西,叫做\(trans\)函數,\(trans(s,c)\)代表從狀態s加上一個字符c所得到的新狀態。
這個的性質不是很多,而且很好理解,就不多談了。
另外,對於一個狀態p我們其實只需要記錄對應區間中的r,因為l為p.parent.r+1。
構造方法
我們采用在線的增量構造方法。
如果原來的字符串是s,那么p代表包含整個字符串s的狀態。
加入的字符是x。
新設np為包含sx的狀態。
那么如果\(trans(p,x)==null\),那么\(trans(p,x)=np,p往上跳parent\)。
如果一直沒有值,那么np.parent顯然為root.
如果有,令\(q=trans(p,x)\)。
如果\(q.r=p.r+1\),那么我們直接令\(np.parent=q\)
如果沒有,現在p.r+1的地方的\(R_x\)會多一個后綴出來,這時候就需要新構造一個狀態出來,更新\(R\)。
具體實現可以參照代碼,十分好理解。
int tot=1,last=1;
struct Node
{
int ch[26];
int fa,len;
}t[MAX<<1];
void add(int x)
{
int p=last,np=last=++tot;
t[np].len=t[p].len+1;
while(p&&!t[p].ch[x])t[p].ch[x]=np,p=t[p].fa;
if(!p)t[np].fa=1;
else
{
int q=t[p].ch[x];
if(t[p].len+1==t[q].len)t[np].fa=q;
else
{
int nq=++tot;
t[nq]=t[q]; t[nq].len=t[p].len+1;
t[q].fa=t[np].fa=nq;
while(p&&t[p].ch[x]==q)t[p].ch[x]=nq,p=t[p].fa;//向上把所有q都替換成nq
}
}
}
