後綴自動機學習筆記

學了一週後綴自動機,覺得...好難啊(主要還是自己太弱了...)

看見網上很多大佬的講解,感覺總是有些似懂非懂,索性一起拿出來做一個總結,可能效果會好一些

首先,我們能看到這樣一個定義:

後綴自動機是一個的確定性有限狀態自動機,能接受這個字符串的所有後綴

然後就不知道了......

(不得不承認,對於我這種蒟蒻,看到這個定義的第一反應是看看別的...)

所以我們直接從後綴自動機的結構與性質入手

後綴自動機是一個有向無環圖(這是第一個要點!!!因爲大部分的字符串自動機都是樹形結構,但這裏不是!!!)

那麼,後綴自動機上的點代表着什麼呢?

一個後綴自動機上的點是一個壓縮節點,它代表這一個字符串!

舉個例子:

(以下的模板串均爲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;
}

(考慮增量構造法的原理,後綴自動機在構造時顯然應該一個一個插入)

這樣的話後綴自動機的構造就結束了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章