前(fei)言(hua):
咳咳,既然是學習筆記,那肯定是邊學邊寫的啊,所以會持續更新呀。
SAM是啥子玩意?(在大佬講之前完全聽都沒聽過,但是根據PPT的排布以及講的是字符串主題來看,這應該是“后綴自動機”了(本蒟蒻聽過后綴自動機,僅僅只是聽過,但是SAM這個名字就沒聽過了))
然后我百度了一下SAM,搜出來這玩意......
噫!好!我人傻了!當場爆粗口來了句氧化鈣。
然后我耐心(氣急敗壞)的查了查“后綴自動機”|,終於正常了。
正題:
后綴自動機 (suffix automaton, SAM) 是一個能解決許多字符串相關問題的有力的數據結構(”高級算法“)。
后綴自動機中幾個重要的概念:
1.endpos(S)表示的是子串S在原串中出現的位置(末位置),例如在ababa中,endpos(ab)={2,4}(好像也有稱為right集合的,不管了),這玩意是個等價集
2.如果存在子串T以及S使得endpos(T)=endpos(S),那么我們則將S以及T歸為同一類中,endpos相等的子串歸在一起稱為一個"狀態",
我們給各個"狀態"依次編號,那么就會得到若干個狀態,在這里我們約定我們記為Str(x)表示狀態x的所有字符串(x是狀態的編號)
同樣拿上面的例子來說事,endpos(ab)={2,4},endpos(b)={2,4},這樣我們將"ab"以及"b"歸為一類,稱為一個"狀態"
3.后綴鏈接,這玩意下文詳細講
4.parent樹,這玩意下文詳細講
后綴自動機的幾個性質
先看一組endpos的數據:
當原串為:“abbabaa”的時候
狀態1. 空串 endpos={1,2,3,4,5,6,7}
狀態2. a endpos={1,4,6,7}
狀態3. ab endpos={2,5}
狀態4. b endpos={2,3,5}
狀態5.abb,bb endpos={3}
狀態6.abba,bba endpos={4}
狀態7.ba endpos={4,6}
狀態8.abbab,bbab,bab endpos={5}
狀態9.abbaba,bbaba,baba,aba endpos={6}
狀態10.abbabaa,bbabaa,babaa,abaa,baa,aa endpos={7}
話說,泥萌有沒有發現什么規律。
對於每一個狀態,里面每一個串的長度都是逐一遞減的,而且后一個串就是前一個串去掉第一個字符后得到的后綴
那么性質一來了:
如果endpos(S)=endpos(T),那么S為T的后綴或者T為S的后綴
不妨令|S|<|T|,那么S一定是T的后綴
證明:如果S不是T的后綴,那么S的第1到len(S)的字符中必然有一位與對應的T的第len(T)-len(S)+1到len(T)位不同
因為endpos(S)=endpos(T),顯然與以上假設矛盾,那么不可能有S的第1到len(S)的字符中必然有一位與對應的T的第len(T)-len(S)+1到len(T)位不同
所以如果endpos(S)=endpos(T),那么S為T的后綴或者T為S的后綴.
性質二:
觀察得到,如果S為T的后綴,則endpos(S)包含集合endpos(T)
如果S不是T的后綴,則endpos(S)∩endpos(T)=∅
性質三(轉移函數)**:
挺重要的。
對於一個狀態X,我們在屬於它的串后面加上一個字符ch,只要這個字符的是屬於由 endpos(狀態X中的字符串)后一個的字符組成的集合nxt,
那么就滿足產生的新串都屬於同一個狀態(這里可能我表述不大清楚,多給幾個例子,方便理解)
例如上面的例子:
對於開篇的串的狀態6,endpos()={4},那么nxt集合等於{"b"}
那么定然abba+b,bba+b屬於一個同一個狀態
對於串“abdabe”中
某個狀態為: ab,b endpos={2,5}
那么ab+d與b+d一定是屬於同一類,
ab+e 與 b+e一定是同一類。
對於串"acbabacba"中
某個狀態為:acba,cba endpos={4,9}
那么對acba+b 與 cba+b一定是同一類的
那么我們將會得到一個函數:f(S,c)=str(S)+(c∈nxt),S為狀態號,nxt可以通過endpos求得,也就是s[endpos(str(S))+1]所得到的字符集
這個函數將會用來進行匹配以及構建SAM!!!!
性質四:
前面說了“對於每一個狀態,里面每一個串的長度都是逐一遞減的,而且后一個串就是前一個串去掉第一個字符后得到的后綴”,那么我們觀察得到每個狀態都是“不完全的”
所謂的不完全就是無法直接從狀態中的最長串一直延續到空串
例如“狀態9.abbaba,bbaba,baba,aba endpos={6}”
如果它是完全的,那么就會有接下來的:
“狀態9.abbaba,bbaba,baba,aba,ba,a endpos={6}”,
顯然a與ba的endpos不與前面幾項endpos相同對不對?所以它們不是同一個狀態
我們要是強行將狀態9完善呢?那么就會需要將“ba”以及“a”接進來,那么就需要一條鏈,這條鏈為:
狀態9----->狀態7------>狀態2----->狀態1
這條鏈肯定是唯一性的(不會有一模一樣的兩條鏈),並且被指向的那個狀態所包含的字符串一定是當前狀態 所有*(一定是的,因為串中字符串的長度是嚴格遞減的) 字符串的后綴。
同時這些鏈它們的終點一定會空集,也就是狀態1,所以,把狀態1看成根節點的話,若干條這樣的鏈也就構成了一棵樹,也就是大名鼎鼎的parent樹,我們驚奇的發現我們要的后綴自動機的節點就是parent樹的節點!
bb了這么多(我太菜了,因為是邊理解邊寫的,可能會有些誤差,希望大佬們能夠指出),現在終於到了講構造后綴自動機的時候了(激動的小手准備開始打板子了)!
還是先講一下構造的思路(只貼代碼不講思路的博客都是耍流氓):
后綴自動機的構造是在線的,我們通過每次插入節點的方式來實現(有之前手寫二叉堆內味了)
- 令 last 為添加字符 c 之前,整個字符串對應的狀態(一開始我們設 last=0 ,算法的最后一步更新 last )。
- 創建一個新的狀態 cur,並將 len(cur) 賦值為 len(last)+1 ,在這時 link(cur) 的值還未知。
- 現在我們按以下流程進行(從狀態 last 開始)。如果還沒有到字符 c的轉移,我們就添加一個到狀態 cur 的轉移,遍歷后綴鏈接。如果在某個點已經存在到字符 c 的轉移,我們就停下來,並將這個狀態標記為p 。
- 如果沒有找到這樣的狀態 p ,我們就到達了虛擬狀態 −1 ,我們將 link(cur) 賦值為 0並退出。
- 假設現在我們找到了一個狀態 p ,其可以通過字符 c 轉移。我們將轉移到的狀態標記為 qqq 。
- 現在我們分類討論兩種狀態,要么 len(p)+1=len(q) ,要么不是。
- 如果 len(p)+1=len(q),我們只要將 link(cur) 賦值為 q 並退出。
- 否則就會有些復雜。需要 復制 狀態 q :我們創建一個新的狀態 clone ,復制 q的除了 len 的值以外的所有信息(后綴鏈接和轉移)。我們將len(clone) 賦值為 len(p)+1 。
復制之后,我們將后綴鏈接從 cur 指向clone ,也從 q 指向 clone 。
最終我們需要使用后綴鏈接從狀態 p 往回走,只要存在一條通過 ppp 到狀態 q 的轉移,就將該轉移重定向到狀態 clone 。 - 以上三種情況,在完成這個過程之后,我們將 last 的值更新為狀態cur 。
以上帶點內容借鑒(抄襲)於:[ HatsuneMiku神佬的題解](https://www.luogu.com.cn/problem/solution/P3804)
代碼就先咕咕了。。。。。
鳴謝:sh妹,Rothen,濤隊,萬總幫助小蒟蒻MYCui