字尾自動機(SAM)速成手冊!
正好寫這個部落格和我的某個別的需求重合了。。。我就來講一講SAM啦qwq
字尾自動機,也就是SAM,是一種極其有用的處理字串的資料結構,可以用於處理幾乎任何有關於子串的問題,但以學起來異常困難著稱(在機房裡,最先學會SAM的永遠是大佬(比如litble和zyf(他在退役前就學了)))。
但是!!!當你學了SAM並熟練地刷了幾道題後,你會發現——你之前為了學SAM而強行理解的許多定理,對你應用SAM一點用處也沒有!為了引出構造演算法,幾乎所有部落格都會詳細地解釋“你為啥要這樣做”,然鵝。。。
SAM完全可以當成黑盒來用!!!!!!
所以我打算寫一篇SAM速成部落格。。。保證即學即會!
在構建之前你不得不知道的
Warning:想徹底理解字尾自動機嗎?那你有好訊息了!請立即關閉此頁面,在百度裡搜尋“字尾自動機 陳立傑”,開始愉快的學習吧!(講真,陳老師的ppt是講的最好的,別的部落格無能出其右者)
SAM是一個DAG(有向無環圖),每個點代表一個"狀態",邊代表狀態轉移,邊上有一個字母。SAM有一個起始狀態(稱為起點),從起點開始,沿著邊不斷走下去,就可以得到一個字串。記當前停留節點為\(x\),走出來的字串為\(S\),稱節點\(x\)可代表字串\(S\)。記\(x\)可代表的串中長度最長的串的長度為\(len(x)\)。
另外,除起點外的每個節點還擁有一個“字尾連結“
儲存SAM利用的是類似於Trie樹的儲存結構,即使用\(ch[][26]\)陣列儲存狀態轉移的邊。
知道了這些,構建SAM的工作就可以開始了。
開始建造字尾自動機
準備工作:建立陣列\(ch,fa,len\),準備指標\(last,cnt\)。SAM的構造方法是不斷地向已經建好的SAM中加入新的節點。\(last\)表示上一個被插入的節點,\(cnt\)表示SAM中的節點數量。一開始,\(last=cnt=1\),表示只有一個起點的初始SAM。
接下來,假設要往SAM里加入一個字元\(x\)。
- 新建節點\(np=++cnt\)
- 如果不存在\(ch[p][x]\),令\(ch[p][x]=np,p=fa[p]\)。重複此步驟。
- 如果到最後還沒有一個\(p\)擁有兒子\(x\),令\(fa[np]=1\)。退出過程。
- 當\(ch[p][x]\)出現時,令\(q=ch[p][x]\)。如果\(len[q]==len[p]+1\),令\(fa[np]=q\)。退出過程。
- 否則有點麻煩。新建節點\(nq=++cnt\),將\(q\)的兒子都複製給\(nq\),令\(len[nq]=len[p]+1\)。
- 令\(fa[nq]=fa[q],fa[q]=fa[np]=nq\)。
- 從\(p\)開始沿著字尾連結,將所有\(ch[p][x]==q\)的節點的\(ch[p][x]\)都替換成\(nq\)。
將你的字串的所有字元都一一進行如上操作後,你就得到了用你的字串構建出來的SAM。
你不需要知道為什麼這麼操作可以得到SAM,你只需要記下以下的程式碼,做幾道題強化記憶,然後就可以用SAM的性質來秒題了。
void insert(int x)
{
int np=++cnt,p=last;
len[np]=len[p]+1,last=np;
while(p&&!ch[p][x])ch[p][x]=np,p=fa[p];
if(!p)fa[np]=1;
else
{
int q=ch[p][x];
if(len[q]==len[p]+1)fa[np]=q;
else
{
int nq=++cnt;len[nq]=len[p]+1;
memmove(ch[nq],ch[q],sizeof(ch[nq]));
fa[nq]=fa[q],fa[np]=fa[q]=nq;
while(ch[p][x]==q)ch[p][x]=nq,p=fa[p];
}
}
}
字尾自動機的奇妙性質
現在,你已經擁有SAM了,你需要知道它有什麼用。這裡列舉了SAM的一些基本且常用的性質。
請牢記以下每一條內容!都十分有用!不要去問“為什麼是這樣的”!(如果一定要問,請參照上文藍色放大的Warning)
首先,SAM的點數與邊數都是\(O(n)\)的。記住,由於每次插入最多新建兩個點,所以應該開字元總量兩倍的空間。計算空間時別忘了你開了26倍的\(ch\)陣列。
在SAM上從起點開始沿著邊隨便走走,得到的一定是子串。同時,每一個子串都可以在SAM上走出一條唯一對應的路徑。也就是說,子串和SAM上從起點開始的每一條路徑一一對應。路徑數等於子串數。
起點可以看做是代表空串的點。
重點:定義子串的\(right\)集合:這個子串在原串中所有出現的位置的右端點的集合。
比如說:AAAABBAAAAABAAABBAA
子串AAB出現了3次,右端點集合為\(\{5,12,16\}\)。這就是子串AAB的\(right\)集合。
一個節點能夠代表的所有子串的\(right\)集合是一樣的。\(right\)集合相等的子串一定被同一個節點代表。(所以,我們會使用“節點的\(right\)集合”這個說法。)兩個節點的\(right\)集合之間要麼真包含,要麼沒有交集。若節點\(y\)的\(right\)集合包含了節點\(x\)的\(right\)集合,那麼\(y\)能代表的子串均為\(x\)能代表的子串的真字尾。
重點:定義節點\(x\)的字尾連結\(fa(x)\):如果有一些節點的\(right\)集合包含了\(x\)的\(right\)集合,\(fa(x)\)是其中\(right\)集合的大小最小的那一個。
字尾連結們組成了一棵“字尾連結樹”(不是字尾樹)。字尾連結樹的根為起點。若節點\(y\)的\(right\)集合包含了節點\(x\)的\(right\)集合,那麼\(y\)在後綴連結樹上是\(x\)的祖先。
一個節點的\(right\)集合等於他在後綴連結樹上的所有兒子的\(right\)集合的並集。而且兒子的\(right\)集合之間兩兩沒有交集。
每個節點能代表的子串的長度範圍是一段連續的區間。這很好理解,因為它們的結束位置都是相同的。
我們求出每個節點能代表的最長串的長度(即\(len(x)\))了,那最短長度呢?其實就等於字尾父親節點的\(len+1\)。也就是說,所有本質不同的子串的數量等於\(\sum len(x)-len(fa(x))\)。
總結
以上就是SAM的基本性質~對於一道特定的題,你可能需要通過上面的性質推出你需要的新性質。如果你還有什麼疑問可以向我留言,我(在退役前)會在一天之內回覆的!(你也可以去問更強的boshi和litble,別去問zyf因為他已經退役了。)
題單我就不給了,因為網上有很多很多。。。
當然,如果你立志要當大佬。。。那趕緊開啟陳立傑的ppt吧=。=
感謝您的觀看qwq!