后綴自動機的一點點理解
前言
最近心血來潮,想學學SAM,於是花了一晚上+一上午
勉強打了出來(但是還是不理解)
雖說張口就講我做不到
但是一些其他的東西還是有所感觸的
索性,亂寫點東西,寫寫關於SAM的一些簡單的理解
資料
一些概念
這些概念都不讀懂,接下來真的是步履維艱
本來我們要的是一個能夠處理所有后綴的數據結構
但是我們發現,如果對於每一個后綴都要插入進Trie樹
空間復雜度完全背不動(\(O(n^2)\)級別)
於是,后綴自動機出現了
后綴自動機相比於Trie樹
在空間上有了很大的改善,他的空間復雜度是\(O(n)\)級別的
(詳見麗潔姐的PPT)
雜七雜八的沒有什么太多寫的必要,網上一找一大堆
寫寫一些概念
right/endpos
hihocoder上寫的是\(endpos\)集合
其他的大部分地方寫的是\(right\)集合
這就是最基礎的概念了
叫做\(endpos\)的話應該很好理解,所以我就寫\(endpos\)吧
\(endpos\)就是一個子串結束位置組成的集合
對於所有結束位置相同的子串
也就是\(endpos\)相同的兩個子串
他們一個一定是另一個的后綴
至於證明,簡單的想一下,如果一個子串出現在了若干個位置
那么他的后綴也一定出現在了這些位置(只可能出現在更多未知,不可能更少)
同時,得到了一個推論:
兩個字符串如果有一個是另一個的后綴,
那么,較長串的\(endpos\)一定是較短串的\(endpos\)的子集
(就是上面寫的,只可能多,不可能少)
同樣的,如果沒有后綴的關系,那么它們的\(endpos\)的交集一定是空集
而后綴自動機的每個節點就是依照\(endpos\)來划分
對於\(endpos\)相同的子串,我們可以划分在一起
我們不難得出一點,對於一堆\(endpos\)相同的子串
他們一定互為后綴,並且他們長度連續
首先證明互為后綴,那就是上面的那個推論,
如果不是互為后綴的話,\(endpos\)就不可能相等
而長度連續?
既然互為后綴,那就一定有一個最長的串,不妨記為\(longest\)
那么,所有的其他串一定是他的后綴
隨着后綴長度的減小,
那么從某一個后綴開始,就可能出現在了更多的位置
那么,這個后綴以及比它更短的后綴的\(endpos\)一定會變大
此時他們就會分到別的節點去了
因此,具有相同\(endpos\)的子串一定長度連續,互為后綴
另外一個簡單的結論,確定了\(endpos\)和長度\(len\)就能確定唯一的子串
trans
\(trans\)不難理解是轉移的意思
設\(trans(s,c)\)表示當前在\(s\)狀態,接受一個字符\(c\)之后所到達的狀態
一個狀態\(s\)表示若干\(endpos\)相同的連續子串
那么,此時相當於在后面加上了一個字符\(c\)
那么,我們對於任意一個串直接加上一個字符\(c\)之后
組成的串的\(endpos\)還是相同的
所以\(trans(s,c)\)就會指向這個狀態
換句話說,隨便在當前狀態\(s\)中找一個串(比如\(longest\))
然后在后面接上一個\(c\)
那么,就指向包含這個新字符串的狀態
Parent/Suffix Links
本質上也是一個東西,不同的地方寫的不一樣而已
不妨設一個狀態中包含的最短的串叫做\(shortest\)
那么,我們就知道\(shortest\)的任意一個非自己的后綴一定就會出現在了更多位置
他的最長的那個后綴,也就是減去了第一個字符后的串
就會出現在另外一個狀態里面,並且是那個狀態的\(longest\)
為什么?因為出現在了更多的位置,我們還是知道他是連續的子串
如果存在一個更長的串
那么,只可能是當前狀態的\(shortest\),
但是\(shortest\)屬於當前狀態,而沒有出現在更多的位置
因此,\(longest\)一定是當前狀態的\(shortest\)減去最前面字符形成的串
那么,當前位置的\(parent\)就會指向那個狀態
當然,還是有幾個很有趣的性質
假設當狀態是\(s\)
\(s.shortest.len=parent.longest.len+1\)
這個就是前面所說的東西,所以,對於每個狀態,就沒有必要記錄\(shortest\)
因為你只要知道\(parent\)就可以算出來了
其次,\(s\)的\(endpos\)是\(parent\)的子集
這個不難證明,因為\(parent\)包含了更多的位置
如果\(trans(s,c)\neq NULL\)
那么,\(trans(parent,c)\neq NULL\)
因為如果\(trans(s,c)\)存在這個狀態
那么\(parent\)的串加上\(c\)之后,一定還是\(s+c\)后的后綴
所以也一定存在\(trans(parent,c)\)
所以,你可以認為\(parent\)是一個完全包含了\(s\)的狀態
也正因為如此,\(parent\)的\(endpos\)就是所有兒子\(endpos\)的並集
將所有的\(parent\)反過來,我們就得到了\(parent\)樹
如果要處理什么,就需要\(parent\)樹的拓撲序
(因為\(parent\)相當於包含了所有的他的子樹,都需要更新上去)
其實不需要拓撲排序
我們知道\(s\)的\(endpos\)完全被\(parent\)的\(endpos\)包含
\(s.longest\)一定長於\(parent.longest\)
所以,一個狀態的\(longest\)越長,它一定要被更先訪問
所以,按照\(longest\)的長度進行桶排序就可以解決拓撲序了
extend
對於一個\(SAM\)的構造
我們當然在線了(因為我只會這個)
我們依次加入字符\(c\),來進行構造
假設原來的字符串是\(T\)
首先,一定會有一個新節點
因為新加入了一個字符后,一定出現了這個新的字符串\(T+c\)
此時\(endpos\)一定是新的位置
同時,原來的\(T\)的最后一個位置也可以通過\(+c\)變到這個新位置
設原來的最后一個位置的狀態是\(last\),新的狀態是\(np\)
所以\(trans(last,c)=np\)
根據前面的東西,我們知道\(last\)的祖先們一定也會有這個\(trans\)
我們要怎么解決他呀
令\(p=last\)
一直沿着\(parent\)往前跳,也就是不斷令\(p=p.parent\)
所以\(p\)代表的,就是越來越短的\(T\)的后綴
因為要更新的是最后的位置,
只有當存在\(T\)的最后一個位置時才能更新
如果\(trans(p,c)=NULL\),直接令\(trans(p,c)=np\)
很顯然是可以直接在后面添加一個\(c\)到達\(np\)的
如果跳完后發現沒有\(parent\)了,直接把\(np.parent\)指向\(1\)
也就是空串所代表的狀態
如果某個\(trans(p,c)\)不為\(NULL\)
那么,設\(q=trans(p,c)\)
如果有\(longest(p)+1=longest(q)\)
什么意思?
在\(p\)的串后面添上一個\(c\)之后就是\(q\)狀態
沒有任何問題,直接在作為\(T\)的后綴的那一個子串上
直接添加一個\(c\)顯然也可以到達\(q\)狀態
又因為\(np\)所代表的\(endpos\)更小,
所以\(np.parent=q\)
在否則的話
也就是\(longest(q)>longest(p)+1\)
具體的反例看麗潔姐PPT第\(35\)頁
如果直接插入的話(也就是\(np.parent=q\))
相當於給\(q\)的\(endpos\)強行插入一個\(np\)
但是,我們發現,如果強行插入進去
這個\(T+c\)的后綴會出現在更多的位置,應該屬於另外一個狀態
然后就\(GG\)了
此時,我們新建一個點\(nq\)
相當於把\(q\)拆成兩部分:
一部分是\(T+c\)的那個后綴,一個是\(longest(p)+c\)
也就是\(longest(nq)=longest(p)+1\)
顯然\(T+c\)的后綴是包含了狀態較少的,
拆分出來的一部分\(q\)是長度較長的
所以\(q.parent=np.parent=nq\)
同時,繼續沿着\(p\)的\(parent\)往上走
把所有的\(q\)都替換成\(nq\)
看起來很有道理,但是我也是似懂非懂的感覺
End
這就是我自己的一些沒有什么用的總結了
我覺得題目才能真正反映SAM的作用
到時候再補點題目上去
補一份后綴自動機\(extend\)的代碼
int tot=1,last=1;
struct Node
{
int son[26];
int ff,len;
}t[MAX<<1];
void extend(int c)
{
int p=last,np=++tot;last=np;
t[np].len=t[p].len+1;
while(p&&!t[p].son[c])t[p].son[c]=np,p=t[p].ff;
if(!p)t[np].ff=1;
else
{
int q=t[p].son[c];
if(t[p].len+1==t[q].len)t[np].ff=q;
else
{
int nq=++tot;
t[nq]=t[q];t[nq].len=t[p].len+1;
t[q].ff=t[np].ff=nq;
while(p&&t[p].son[c]==q)t[p].son[c]=nq,p=t[p].ff;
}
}
}