學了一週後綴自動機,覺得...好難啊(主要還是自己太弱了...)
看見網上很多大佬的講解,感覺總是有些似懂非懂,索性一起拿出來做一個總結,可能效果會好一些
首先,我們能看到這樣一個定義:
後綴自動機是一個的確定性有限狀態自動機,能接受這個字符串的所有後綴
然後就不知道了......
(不得不承認,對於我這種蒟蒻,看到這個定義的第一反應是看看別的...)
所以我們直接從後綴自動機的結構與性質入手
後綴自動機是一個有向無環圖(這是第一個要點!!!因爲大部分的字符串自動機都是樹形結構,但這裏不是!!!)
那麼,後綴自動機上的點代表着什麼呢?
一個後綴自動機上的點是一個壓縮節點,它代表這一個字符串!
舉個例子:
(以下的模板串均爲abbab)
這是一個建好的後綴自動機
(不要介意它的長相,我們一會介紹如何構造)
那麼我們看一下,如果我們真正插入所有後綴,那麼這裏的時間和空間都是無法接受的O(n^2)級別
所以我們需要將後綴進行壓縮,壓縮後即得到上圖
(這裏有一個吐槽:雖然這個東西叫“後綴”自動機,但是其大部分原理更接近於一個字符串的子串是一個前綴的後綴,所以或許“前綴”自動機這個名字更合適?)
可能你並沒有理解,我們舉個例子:
比如對於後綴“bbab“和“abbab”,如果我們按照傳統的方式建立後綴樹的話,那麼我們需要建立9個節點
但是,我們發現,這兩個後綴後3個字母是一樣的(其實嚴格意義上,後4個字母都一樣,但我們爲了能從根節點識別到這個後綴,所以我們要單獨提出第一個字符,這裏在下面構造的時候有一個很重要的討論,先留下)
因此,我們完全沒有必要建立那麼多節點,而是直接將節點b剩下的部分指向ab以下已經建好的部分,這樣就實現了壓縮空間!
而藍色的線,我們把它叫做pre指針,它指向的是當前節點對應字符串的最長後綴對應的節點
它是非常有用的,這個作用會在應用中具體體現
接下來我們談一談構造:
後綴自動機的構造方式稱爲增量構造法,也即在一個已經構造好的後綴自動機上去增加一個字符,看看會發生什麼
接下來開始構造
首先,我們在根節點上插入第一個節點a
情況大概是這個樣子
藍色的線即爲pre指針,很顯然,這裏最長後綴並沒有對應的節點,所以直接指回根節點
注意第一個節點上的標號,它並不是節點編號,而是一個叫len的量,具體含義及作用下面會談到。
然後我們插入第二個節點:
大概就是這個樣子
注意到由於ab最長後綴也找不到,所以pre同樣指回根節點,而考慮到b也是一個後綴的開頭,所以要補全根節點到b的出邊
然後插入第三個節點
如果我們按照樸素思想構造的話,情況應該是這個樣子
但是,這樣做是否存在問題呢?
爲了探究這個問題,首先我們要思考:前兩個節點的pre指針是如何構造出來的?
雖然在圖上直接找看似一目瞭然,但是真正實現並不容易!
具體的方法是,我們順着上次插入的節點的pre指針一路向上跳,對跳到的每個節點,將它的對應指針指向這個新來的點。
具體解釋一下:我們順着pre指針向上跳的時候,跳到的每個點對應的都是這個字符串的一個後綴,同時他也在整個串中作爲一個前綴出現過,所以我在新引進了一個字符以後,等價於在原有後綴後面加入了一個字符,自然也就等價於在這個串的最長後綴的後面加一個字符,也就是補全了兒子指針
可能文字並不清楚,我們給出一個具體例子:
比如串abac,已經插入了aba,那麼可以看到,這個串的最長後綴(能連出pre指針的)應當是a
那麼,如果我們繼續插入一個c,那麼得到的新串一定有一個後綴是ac(這裏很顯然)
那麼出於壓縮空間的考慮,我們可以直接由pre指針指向的那個a向新來的c連邊,這樣就可以直接從根開始識別後綴了
如果畫出圖大概就是這樣:
可以看到,塗成紅色的線就是我所提到的情況,直接從第一個a引出一條c邊構成後綴ac
所以這裏會有一個處理的操作
於是就涉及到了幾個新的問題:
第一,如果順着pre指針向上跳的時候,我們沒有找到任何一個節點有與新來的節點相同的出邊,那麼這時很好處理,僅需將新來的節點pre指針指向根節點即可。
就像這個圖,由於順着a的pre指針向上跳的時候沒有任何一個節點有出邊b,所以直接將新節點的pre指針指向根節點即可。
第二,也是比較複雜的情況:如果跳到的某個節點竟然有相同的出邊,這該怎麼辦?
就比如這種情況,我們順着第一個b的pre指針向上跳的時候竟然驚喜的發現,它的pre指針有一個節點b!!!
這咋辦?
基於樸素的思想,我們可以直接將新節點的pre指針指向那個節點b,就像上圖給出的那樣。
可是回到最初的問題:這樣做合理嗎?
很容易可以發現: 不合理!
爲什麼?
這時就體現出了上面提到的len變量的作用:我們看到,原先的b節點是一個壓縮節點,考慮他的len並不等於他pre指針的len+1,也就意味着從他的pre指針到他之間壓縮了信息!
(具體可以理解成壓縮了ab和b兩個串)
而當我們插入一個新節點以後,基於定義:pre指針指向了它所對應字符串的最長後綴
可...等等,這個最長後綴是哪一個呢?
是b,還是ab?
由於指向的b是一個壓縮節點,所以我根本無法得知這個最長後綴是誰啊!
這也就產生了信息的丟失和混亂
所以我們直接這樣去操作是不合理的
那怎麼辦?
可以看到,指向的b是壓縮節點的原因就體現在len的關係上,由於b的len不等於它的pre的len+1,這意味着狀態的壓縮,也就意味着直接插入會導致信息的丟失
那麼我們換一個思想:如果b的len等於他pre的len+1會怎樣?
不難看出,此時直接連接pre指針就是合理的了!
因爲此時可以說明中間沒有壓縮節點,也就是說在走後綴的時候是不會產生上面說的問題的了!
那麼我們仿照這種思想,如果我將壓縮節點展開,不就不會出現信息的丟失了嗎?!
這就是後綴自動機構造中最爲核心的一環!
回到上面的例子,那此時合理的做法應當是將b節點展開,構造一個新的b節點去接受pre指針
所以更合理的情況應當是這樣:
其中用綠色給出了分裂出的節點
剩下的問題就好辦了
我們再插入一個節點:
可以看到,這就是我們上面提到的len恰好等於上一個len+1的情況,所以不需要分裂節點,只需建立pre指針即可。
再插入一個:
可以發現,這就是最開始我們畫出來的那臺後綴自動機了
所以我們的構造就完成了
貼個構造的代碼
struct SAM
{
int tranc[27];
int len;
int pre;
int v;
int endpos;
}s[5000005];
int siz;
int las;
void ins(int c)
{
int nwp=++siz;
s[nwp].len=s[las].len+1;
s[nwp].endpos=1;
int lsp;
for(lsp=las;lsp&&!s[lsp].tranc[c];lsp=s[lsp].pre)s[lsp].tranc[c]=nwp;
if(!lsp)
{
s[nwp].pre=1;
}else
{
int lsq=s[lsp].tranc[c];
if(s[lsq].len==s[lsp].len+1)
{
s[nwp].pre=lsq;
}else
{
int nwq=++siz;
s[nwq]=s[lsq];
s[nwq].endpos=0;
s[nwq].len=s[lsp].len+1;
s[lsq].pre=s[nwp].pre=nwq;
while(s[lsp].tranc[c]==lsq)
{
s[lsp].tranc[c]=nwq;
lsp=s[lsp].pre;
}
}
}
las=nwp;
}
(考慮增量構造法的原理,後綴自動機在構造時顯然應該一個一個插入)
這樣的話後綴自動機的構造就結束了