在pongba的討論組上看到一道Amazon的面試題:找出給定字符串裏的最長迴文。例子:輸入XMADAMYX。則輸出MADAM。這道題的流行解法是用後綴樹(Suffix Tree)。這坨數據結構最酷的地方是用它能高效解決一大票複雜的字符串編程問題: 面試
在文本T裏查詢T是否包含子串P(複雜度同流行的KMP至關)。算法
文本T裏找出最長重複子串。好比abcdabcefda裏abc同da都重複出現,而最長重複子串是abc。編程
找出字符串S1同S2的最長公共子串。注意不是經常使用做動態規劃例子的LCS哈。好比字符串acdfg同akdfc的最長公共子串爲df,而他們的LCS是adf。數據結構
Ziv-Lampel無損壓縮算法。優化
還有就是這道面試題問的最長迴文了。spa
另外後綴樹在生物信息學裏應該應用普遍。鹼基匹配和選取的計算本質上就是操做超長的{C, T, A, G, U}*字符串嘛。翻譯
雖然說後綴樹的概念獨立於Trie的概念,但我以爲從Trie推出後綴樹天然簡潔,因此先簡單解釋一下Trie。「Trie」這個單詞來自於"retrieve",可見它的用途主要是字符串查詢。不過詞彙變遷多半比較詭異,Trie不發tree的音,而發try的音。字符串
Trie是坨簡單但實用的數據結構,一般用於實現字典查詢。咱們作即時響應用戶輸入的AJAX搜索框時,就是Trie開始。誰說學點數據結構沒用來 着?本質上,Trie是一顆存儲多個字符串的樹。相鄰節點間的邊表明一個字符,這樣樹的每條分支表明一則子串,而樹的葉節點則表明完整的字符串。和普通樹 不一樣的地方是,相同的字符串前綴共享同一條分支。仍是例子最清楚。給出一組單詞,inn, int, at, age, adv, ant, 咱們能夠獲得下面的Trie:hash
能夠看出:原理
每條邊對應一個字母。
每一個節點對應一項前綴。葉節點對應最長前綴,即單詞自己。
單詞inn與單詞int有共同的前綴「in」, 所以他們共享左邊的一條分支,root->i->in。同理,ate, age, adv, 和ant共享前綴"a",因此他們共享從根節點到節點"a"的邊。
查詢很是簡單。好比要查找int,順着路徑i -> in -> int就找到了。
搭建Trie的基本算法也很簡單,無非是逐一把每則單詞的每一個字母插入Trie。插入前先看前綴是否存在。若是存在,就共享,不然建立對應的節點和邊。好比要插入單詞add,就有下面幾步:
考察前綴"a",發現邊a已經存在。因而順着邊a走到節點a。
考察剩下的字符串"dd"的前綴"d",發現從節點a出發,已經有邊d存在。因而順着邊d走到節點ad
考察最後一個字符"d",這下從節點ad出發沒有邊d了,因而建立節點ad的子節點add,並把邊ad->add標記爲d。
繼續插播廣告。Graph做圖軟件Graphviz還不錯,用的DSL至關簡單。上面的圖就是用它作的。三步就夠了:
實現Trie數據結構。這步不用花哨。10行代碼,一坨hash足矣。
把上面的結構翻譯成Graphviz的DSL。簡單的深度優先足矣。
調用Graphviz的命令。圖就生成樂。
多花20分鐘,避免了手工做圖排版的自虐行爲。並且能夠自由試驗各式例子而不用擔憂反覆畫圖的瑣碎,何樂而不爲囁?
有了Trie,後綴樹就容易理解了。先說說後綴的定義。給定一長度爲n的字符串S=S1S2..Si..Sn,和整數i,1 <= i <= n,子串SiSi+1...Sn都是字符串S的後綴。以字符串S=XMADAMYX爲例,它的長度爲8,因此S[1..8], S[2..8], ... , S[8..8]都算S的後綴,咱們通常還把空字串也算成後綴。這樣,咱們一共有以下後綴。對於後綴S[i..n],咱們說這項後綴起始於i。
S[1..8], XMADAMYX, 也就是字符串自己,起始位置爲1
S[2..8], MADAMYX,起始位置爲2
S[3..8], ADAMYX,起始位置爲3
S[4..8], DAMYX,起始位置爲4
S[5..8], AMYX,起始位置爲5
S[6..8], MYX,起始位置爲6
S[7..8], YX,起始位置爲7
S[8..8], X,起始位置爲8
空字串。記爲$。
然後綴樹,就是包含一則字符串全部後綴的壓縮Trie。把上面的後綴加入Trie後,咱們獲得下面的結構:
仔細觀察上圖,咱們能夠看到很多值得壓縮的地方。好比藍框標註的分支都是獨苗,沒有必要用單獨的節點同邊表示。若是咱們容許任意一條邊裏包含多個字 母,就能夠把這種沒有分叉的路徑壓縮到一條邊。另外每條邊已經包含了足夠的後綴信息,咱們就不用再給節點標註字符串信息了。咱們只須要在葉節點上標註上每 項後綴的起始位置。因而咱們獲得下圖:
這樣的結構丟失了某些後綴。好比後綴X在上圖中消失了,由於它正好是字符串XMADAMYX的前綴。爲了不這種狀況,咱們也規定每項後綴不能是其 它後綴的前綴。要解決這個問題其實挺簡單,在待處理的子串後加一坨空字串就好了。例如咱們處理XMADAMYX前,先把XMADAMYX變爲 XMADAMYX$,因而就獲得suffix tree了。
那後綴樹同最長迴文有什麼關係呢?咱們得先知道兩坨坨簡單概念:
最低共有祖先,LCA(Lowest Common Ancestor),也就是任意兩節點(多個也行)最長的共有前綴。好比下圖中,節點7同節點10的共同祖先是節點1與借點,但最低共同祖先是5。 查找LCA的算法是O(1)的複雜度,這年頭少見。代價是須要對後綴樹作複雜度爲O(n)的預處理。
廣 義後綴樹(Generalized Suffix Tree)。傳統的後綴樹處理一坨單詞的全部後綴。廣義後綴樹存儲任意多個單詞的全部後綴。例以下圖是單詞XMADAMYX與XYMADAMX的廣義後綴 樹。注意咱們須要區分不一樣單詞的後綴,因此葉節點用不一樣的特殊符號與後綴位置配對。
有了上面的概念,查找最長迴文相對簡單了。思惟的突破點在於考察迴文的半徑,而不是迴文自己。所謂半徑,就是迴文對摺後的字串。好比迴文MADAM 的半徑爲MAD,半徑長度爲3,半徑的中心是字母D。顯然,最長迴文必有最長半徑,且兩條半徑相等。仍是以MADAM爲例,以D爲中心往左,咱們獲得半徑 DAM;以D爲中心向右,咱們獲得半徑DAM。兩者確定相等。由於MADAM已是單詞XMADAMYX裏的最長迴文,咱們能夠確定從D往左數的字串 DAMX與從D往右數的子串DAMYX共享最長前綴DAM。而這,正是解決迴文問題的關鍵。如今咱們有後綴樹,怎麼把從D向左數的字串DAMX變成後綴 呢?到這個地步,答案應該明顯:把單詞XMADAMYX翻轉就好了。因而咱們把尋找回文的問題轉換成了尋找兩坨後綴的LCA的問題。固然,咱們還須要知道 到底查詢那些後綴間的LCA。這也簡單,給定字符串S,若是最長迴文的中心在i,那從位置i向右數的後綴恰好是S(i),而向左數的字符串恰好是翻轉S後 獲得的字符串S‘的後綴S'(n-i+1)。這裏的n是字符串S的長度。有了這套直觀解釋,算法天然呼之欲出:
預處理後綴樹,使得查詢LCA的複雜度爲O(1)。這步的開銷是O(N),N是單詞S的長度
對單詞的每一位置i(也就是從0到N-1),獲取LCA(S(i), S(N-i+1)) 以及LCA(S(i+1), S(n-i+1))。查找兩次的緣由是咱們須要考慮奇數迴文和偶數迴文的狀況。這步要考察每坨i,因此複雜度是O(N)
找到最大的LCA,咱們也就獲得了迴文的中心i以及迴文的半徑長度,天然也就獲得了最長迴文。總的複雜度O(n)。
用上圖作例子,i爲3時,LCA(3$, 4#)爲DAM,正好是最長半徑。固然,這只是直觀的敘述。
這篇帖子只大體描述了後綴樹的基本思路。要想寫出實用代碼,至少還得知道下面的知識:
建立後綴樹的O(n)算法。至因而Peter Weiner的73年年度最佳算法,仍是Edward McCreight1976的改進算法,仍是1995年E. Ukkonen大幅簡化的算法,仍是Juha Kärkkäinen 和 Peter Sanders2003年進一步簡化的線性算法,各位老大隨喜。
實現後綴樹用的數據結構。好比經常使用的子結點加兄弟節點列表,Directed
優化後綴樹空間的辦法。好比不存儲子串,而存儲讀取子串必需的位置。以及Directed Acyclic Word Graph,常縮寫爲黑哥哥們掛在嘴邊的DAWG。
(1). 查找字符串o是否在字符串S中。
方案:用S構造後綴樹,按在trie中搜索字串的方法搜索o便可。
原理:若o在S中,則o必然是S的某個後綴的前綴。
例如S: leconte,查找o: con是否在S中,則o(con)必然是S(leconte)的後綴之一conte的前綴.有了這個前提,採用trie搜索的方法就不難理解了。
(2). 指定字符串T在字符串S中的重複次數。
方案:用S+’$'構造後綴樹,搜索T節點下的葉節點數目即爲重複次數
原理:若是T在S中重複了兩次,則S應有兩個後綴以T爲前綴,重複次數就天然統計出來了。
(3). 字符串S中的最長重複子串
方案:原理同2,具體作法就是找到最深的非葉節點。
這個深是指從root所經歷過的字符個數,最深非葉節點所經歷的字符串起來就是最長重複子串。
爲何要非葉節點呢?由於既然是要重複,固然葉節點個數要>=2。
(4). 兩個字符串S1,S2的最長公共部分
方案:將S1#S2$做爲字符串壓入後綴樹,找到最深的非葉節點,且該節點的葉節點既有#也有$(無#)。