問題描述:
后綴樹(Suffix Tree)
參考資料:
http://www.cppblog.com/yuyang7/archive/2009/03/29/78252.html
http://blog.csdn.net/v_july_v/article/details/6897097
簡介
后綴樹是一種PAT樹,它描述了給定字符串的所有后綴,許多重要的字符串操作都能夠在后綴樹上快速地實現。
定義
一個長度為n的字符串S,它的后綴樹定義為一棵滿足如下條件的樹:
1. 從根到樹葉的路徑與S的后綴一一對應。即每條路徑惟一代表了S的一個后綴;
2. 每條邊都代表一個非空的字符串;
3. 所有內部節點(根節點除外)都有至少兩個子節點。
由於並非所有的字符串都存在這樣的樹,因此S通常使用一個終止符號進行填充(通常使用$)。
優點
1. 匹配快。對於長度為m的模式串,只需花費至多O(m)的時間進行匹配。
2. 空間省。Suffix tree的空間耗費要低於Suffix trie,因為Suffix tree除根節點外不允許其內部節點只含單個子節點,因此它是Suffix trie的壓縮表示。
后綴樹的生成,Suffix trie ------>> Suffix tree
后綴,顧名思義,甚至通俗點來說,就是所謂后綴就是后面尾巴的意思。比如說給定一長度為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--后綴樹了,如下圖所示:
2.2、后綴樹與回文問題的關聯
那后綴樹同最長回文有什么關系呢?我們得先知道兩個簡單概念:
- 最低共有祖先,LCA(Lowest Common Ancestor),也就是任意兩節點(多個也行)最長的共有前綴。比如下圖中,節點7同節點10的共同祖先是節點1與節點,但最低共同祖先是5。 查找LCA的算法是O(1)的復雜度,這年頭少見。代價是需要對后綴樹做復雜度為O(n)的預處理。
- 廣義后綴樹(Generalized Suffix Tree)。傳統的后綴樹處理一坨單詞的所有后綴。廣義后綴樹存儲任意多個單詞的所有后綴。例如下圖是單詞XMADAMYX與XYMADAMX的廣義后綴 樹。注意我們需要區分不同單詞的后綴,所以葉節點用不同的特殊符號與后綴位置配對。
2.3、最長回文問題的解決
有了上面的概念,本文引言中提出的查找最長回文問題就相對簡單了。咱們來回顧下引言中提出的回文問題的具體描述:找出給定字符串里的最長回文。例如輸入XMADAMYX,則輸出MADAM。思維的突破點在於考察回文的半徑,而不是回文本身。所謂半徑,就是回文對折后的字串。比如回文MADAM 的半徑為MAD,半徑長度為3,半徑的中心是字母D。顯然,最長回文必有最長半徑,且兩條半徑相等。還是以MADAM為例,以D為中心往左,我們得到半徑 DAM;以D為中心向右,我們得到半徑DAM。二者肯定相等。因為MADAM已經是單詞XMADAMYX里的最長回文,我們可以肯定從D往左數的字串 DAMX與從D往右數的子串DAMYX共享最長前綴DAM。而這,正是解決回文問題的關鍵。現在我們有后綴樹,怎么把從D向左數的字串DAMX變成后綴 呢?
到這個地步,答案應該明顯:把單詞XMADAMYX翻轉(XMADAMYX=>XYMADAMX,DAMX就變成后綴了)就行了。於是我們把尋找回文的問題轉換成了尋找兩坨后綴的LCA的問題。當然,我們還需要知道 到底查詢那些后綴間的LCA。很簡單,給定字符串S,如果最長回文的中心在i,那從位置i向右數的后綴剛好是S(i),而向左數的字符串剛好是翻轉S后得到的字符串S‘的后綴S'(n-i+1)。這里的n是字符串S的長度。
可能上面的闡述還不夠直觀,我再細細說明下:
1、首先,還記得本第二部分開頭關於后綴樹的定義么: “先說說后綴的定義,顧名思義,甚至通俗點來說,就是所謂后綴就是后面尾巴的意思。比如說給定一長度為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
空字串,記為$。
2、對單詞XMADAMYX而言,回文中心為D,那么D向右的后綴DAMYX假設是S(i)(當N=8,i從1開始計數,i=4時,便是S(4..8));而對於翻轉后的單詞XYMADAMX而言,回文中心D向右對應的后綴為DAMX,也就是S'(N-i+1)((N=8,i=4,便是S‘(5..8)) 。此刻已經可以得出,它們共享最長前綴,即LCA(DAMYX,DAMX)=DAM。有了這套直觀解釋,算法自然呼之欲出:
- 預處理后綴樹,使得查詢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)。
上面大致描述了后綴樹的基本思路。要想寫出實用代碼,至少還得知道下面的知識:
- 創建后綴樹的O(n)算法。此算法有很多種,無論Peter Weiner的73年年度最佳算法,還是Edward McCreight1976的改進算法,還是1995年E. Ukkonen大幅簡化的算法(本文第4部分將重點闡述這種方法),還是Juha Kärkkäinen 和 Peter Sanders2003年進一步簡化的線性算法,都是O(n)的時間復雜度。至於實際中具體選擇哪一種算法,可依實際情況而定。
- 實現后綴樹用的數據結構。比如常用的子結點加兄弟節點列表,Directed 優化后綴樹空間的辦法。比如不存儲子串,而存儲讀取子串必需的位置。以及Directed Acyclic Word Graph,常縮寫為黑哥哥們掛在嘴邊的DAWG。
2.4、后綴樹的應用
后綴樹的用途,總結起來大概有如下幾種- 查找字符串o是否在字符串S中。
方案:用S構造后綴樹,按在trie中搜索字串的方法搜索o即可。
原理:若o在S中,則o必然是S的某個后綴的前綴。
例如S: leconte,查找o: con是否在S中,則o(con)必然是S(leconte)的后綴之一conte的前綴.有了這個前提,采用trie搜索的方法就不難理解了。 - 指定字符串T在字符串S中的重復次數。
方案:用S+’$'構造后綴樹,搜索T節點下的葉節點數目即為重復次數
原理:如果T在S中重復了兩次,則S應有兩個后綴以T為前綴,重復次數就自然統計出來了。 - 字符串S中的最長重復子串
方案:原理同2,具體做法就是找到最深的非葉節點。
這個深是指從root所經歷過的字符個數,最深非葉節點所經歷的字符串起來就是最長重復子串。
為什么要非葉節點呢?因為既然是要重復,當然葉節點個數要>=2。 - 兩個字符串S1,S2的最長公共部分
方案:將S1#S2$作為字符串壓入后綴樹,找到最深的非葉節點,且該節點的葉節點既有#也有$(無#)。
后綴樹實現: