Suffix Tree 學習筆記 I
Author: | If |
---|---|
Date: | 2010/10/3 9:34:06 |
Copyright: | If some rights reserved, published under license Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported. |
Contents
1 Prologue
我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我我是意識流不要讀我
俺(If)是不久之前才接觸到后綴樹這個概念的,而它實在是太迷人了,了解了它的概念之后,我心中曾有過的幾個設想頓時都有見到了光明:
比如說, 我希望能做出一個程序,能找出 C 代碼中出現次數 × 字串長度最大的 相似 序列,然后幫我#define 一下,好在 POJ 代碼長度項上排到第一名;
再比如說, 我還希望做出一個管理自己源代碼用的全文檢索程序,能夠自由的查找含有各種奇特符號的任意代碼段 —— 而不是像倒排索引那樣事先分好詞;
如此這般。
雖然目前為止這些想法都還沒有實現,但我已經看到路了。
另外請原諒我這里喜歡使用英文標題 —— 因為 rst2html 會把英文標題的錨點取名為標題本身,這讓我看着比較爽一些。
2 Notations
- T[ j .. i ]
- 字串 T 的 j 到 i 之間的子串。例如 T="hello", 那么 T[ 0 .. 3 ] = "hell"。
- T[ j .. i ] 的路徑
- T 的子串 T[ j .. i ] 在后綴樹中從根節點開始的路徑。
- STree(T)
- 字符串 T 的后綴樹。
- “在后綴樹中”
- 當我說 “字符串 S 在后綴樹中” 時,我的意思其實是后綴樹有一條 從根節點開始的路徑 S 。
- 階段,擴展
- 構建后綴樹的過程可以分為 n+1 個階段,其中 n = |T|, 第 i 階段有 i 次擴展(原始算法),在后面 How To Build It 那一節再詳細介紹。
3 What is a Suffix Tree
Suffix tree, 或 后綴樹 ,是一種相當神奇的數據結構,它包含了字符串中的大量信息,能夠用於解決很多復雜的字符串問題 —— 事實上,基本上目前為止俺遇到過的所有與字符串有關的問題都可以通過它解決。
我們以字符串 MISSISSIPPI 為例,先列出它所有的后綴:
1. MISSISSIPPI 2. ISSISSIPPI 3. SSISSIPPI 4. SISSIPPI 5. ISSIPPI 6. SSIPPI 7. SIPPI 8. IPPI 9. PPI A. PI B. I
將它們分別插入到 字典樹 中我們可以得到:
BTW, 樹中的 $ 符號表示字符串結尾 —— 這里 I 是字符串 MISSISSIPPI 的結尾,而 I 又在其中間出現了,所以有必要使用一個特殊的符號標識出來,同理在后綴樹中也需要有這樣的一個符號;雖然 C 語言中有'\0' ,但這里使用 $ 符號似乎屬於約定俗成的事情了,咱這次也就從了它吧。
這樣的一棵字典樹已經具備了后綴樹的功能 ——
比如說吧,你從根節點開始順着這個字典樹的隨便一個樹枝走 n 步,然后數一下再往下有幾片葉子,就知道已經走過的這 n 個字符組成的字符串在原字符串中一共出現過多少次了 —— 由於你可以把某一節點下方的葉節點數目保存在該節點中,所以這個算法實際上是 O(n) 的,怎么樣?
只是,你也看出來了,它的空間復雜度相當之高,實際上基本上沒有誰會用這樣一個數據結構。
於是, 把所有最近兩個有多個分支的節點之間的部分(它們每個節點各自僅有一個分支)合並起來 (參考:Patricia Trie 或曰 Radix Tree),我們可以得到:
這就是一棵后綴樹了。
順便提一句,雖然邏輯結構如上,但實際的數據結構中存儲的東西大致是這樣的:
這樣,每一條邊占用的空間為常數大小,於是后綴樹 Tree(T) 的空間復雜度為 O(|T|),|T| 為字符串 T 的長度。 因為 :
- 從樹根到樹葉的每一條路徑組成了一個后綴
- 所以樹葉的數量等於后綴的數量
- 每個非葉子節點都有至少兩條樹枝
- 所以非葉節點的數量小於葉節點的數量
- 除去根節點,節點數等於邊數
- 所以,后綴樹的空間復雜度為: O(|T| + size(edge labels)) [1]
后綴樹和 后綴 Trie 一樣包含了整個字符串中所有子串的信息,又僅占用 O(|T|) 空間,而且 —— 后面會說到,它可以在 O(|T|) 時間內構造出來,這些特性使它成為了一個理論分析以及實際應用上都非常重要的數據結構。
4 What Can It Do
后綴樹的用途是如此之廣以至於 Dan Gusfield 在他的書中 [2] 花了七十多頁來介紹,我在這里先挑幾個自己看來比較好玩的例子說說,以期待激發讀者的興趣。
4.1 Longest Common Substring
Longest common substring - 即 最長公共子串 ,是指在兩個字符串中都出現了的最長子串 —— 比如,superiorcalifornialives 和 sealiver 的最長公共子串為 alive 。
這是個很經典的例子,據說 1970 年,后綴樹的概念還沒有被提出來的時候,Knuth 爺爺曾無比確信找出兩個字符串最長公共子串的線性算法是不存在的 [2] ;然而,在知道后綴樹的概念之后,這個算法也顯而易見了:
- 將兩個字符串 T1, T2 加入到同一個后綴樹,並在節點上標記該節點屬於哪個字符串。
- 遍歷后綴樹(該后綴樹中節點數目不大於 2(|T1|+|T2|),請參照后綴樹空間復雜度的證明 ),找出最深的、同時屬於兩個字符串的節點。
- 從根節點到這個結點的路徑就是它們的最長公共子串了。
4.2 Exact String Matching
-
當字符串 T 已知且固定不變,P 改變時:
我們可以用 O(|T|) 時間預處理 T,然后在 O(|P|) 時間內回答出 “P 在 T 中出現了幾次?” 或是 “P 在 T 的哪些地方出現過?” 之類的問題:
預處理 T, 得到它的后綴樹 —— 也可以把多個字符串 T1, T2 ... 放入同一個后綴樹,然后查找 P 時只需要從這個后綴樹的根開始順着 P 往下走,然后讀出保存在節點中的信息 (下方的樹葉數量、出現地方 &etc.) 就行了。
而這也就意味着你可以把很大的文檔進行 索引 ,然后快速的進行搜索。
-
當字符串 P 已知且固定不變時:
我們也可以用 O(|P|) 時間預處理模式串 P,然后用 O(|T|) 時間在 T 中找出 P —— 時間、空間復雜度的上界就和 KMP 算法或是 Boyer-Moore 算法一樣。
這個可能看不明顯,但是,這么說吧,KMP 算法中 Next 數組保存的所有信息、Boyer-Moore 算法中 Bad-Charactor 和 Good-Suffix 表的信息后綴樹中都有。 由於實際操作上比較復雜,這里就不展開了,讀者請自行參考 [2] 或者耐心等待 If (嗯,也就是本人) 心血來潮的那一天。
-
當 P 和 T 都已知的時候:
很明顯,我們可以在 O(|T| + |P|) 時間內找出 P 在 T 中的出現,不說了。
4.3 Ziv-Lempel Compression
嗯,常說的 LZ77 指的就是 Lempel Ziv 1977。
Ziv-Lempel 壓縮的主要思想是,將前方出現過的子串使用 (位置, 長度) 對 表示出來,例如aaaaaaaaaaaaaaaa 就可以表示為 a(1,1)(1,2)(1,4)(1,8) :
a(1,1) == aa aa(1,2) == aaaa aaaa(1,4) == aaaaaaaa aaaaaaaa(1,8) == aaaaaaaaaaaaaaaa
真正的 LZ 編碼方式和這里說的有點區別,但思想是一樣的。
有了后綴樹,LZ 壓縮也簡單了:
- 對要壓縮的字符串 T 構造出后綴樹 Tree(T) 。
- 遍歷這棵樹,在每個節點 v 上標出該節點下方的所有樹葉中(位置)最小的值 C(v) 。
- 當需要知道位置 i 上的 (位置, 長度) 編碼時:
- 順着字符串 T[ i..m ] 的前綴走到 p 點。
- 設 v 是 p 下方的第一個節點(或者 p 本身,如果 p 是節點的話)。
- 設 d 是 p 的深度。
- 找出滿足 d + C(v) <= i , d 盡可能大的 p 點,則此時從根節點到 p 的路徑就是最長的、同時出現在了 T[ 1..i ] 中的 T[ i..m ] 的前綴。
- 即, T[ C(v)..C(v)+d ] == T[ i..i+d ] 。
- 於是, T[ i..i+d ] 就可以表示成 (C(v), d) 序列了。
以上查找 (位置, 長度) 序列的算法時間復雜度是 O(長度),於是壓縮算法整體復雜度就是 O(|T|) 。
—— BTW,后面會說到,根據 Ukkonen 的算法 [3] ,后綴樹本身可以 "On-line" 構造,即,每次加入一個字符,得到當前的后綴樹;這樣, Ziv-Lempel 壓縮也可以僅用一次自左向右的掃描完成。
5 Few New Concepts
在繼續講如何構造后綴樹之前,為了后面表述的方便,再提幾個簡單的概念:
- Explicit Node (顯式節點) :
- 出現在后綴樹里了的所有節點都可以稱為 顯示節點 。
- Internal Node (內部節點) :
- 為了和葉節點區分開來,我們給不是葉節點的顯式節點起個新名字,叫做 內部節點 。
- Implicit Node (隱式節點) :
- 出現在了 后綴 Trie 中卻沒有出現在后綴樹中的那些節點是 隱式節點 。
- 顯式擴展 :
- 對后綴樹做了點什么
- 隱式擴展 :
- 對后綴樹實際上啥都沒做
6 How To Build It
目前我只弄懂了 Ukkonen 的算法 [3] ,所以下面也只說這一種算法:
6.1 Native Approach
首先,我們觀察發現,若 S 是一個非空字符串, c 是一個字符,則將 c 加到 S 的每個后綴(包括空后綴)后面,我們可以得到 Sc 的所有后綴。
Suffixes(a): a Suffixes(ab): ab b Suffixes(abc): abc bc c Suffixes(abca): abca bca ca a ...
很顯然,可以將這樣的原理用於構建后綴樹:假設 T[0..i-1] 的后綴樹已經建好了,那么在 T[0..i-1] 的每個后綴 T[0..i-1], T[1..i-1] .. T[j..i-1] .. T[i-1..i-1] 的后面加上字符 T[i] 就可以得到 T[0..i] 的后綴樹了。
具體到 “將 T[i] 加到后綴 T[j..i-1]” 的這個過程,則有可能遇到三種情況:
- 在后綴樹中,T[j..i-1] 停在了樹葉上。
這種情況下,只需要把 T[i] 加到該樹葉的邊上即可。 - 在后綴樹中,T[j..i-1] 的后面緊跟着的字符的集合 {a,b,c ... } 中不包含 T[i]。
這種情況下,在 T[j..i-1] 的后面加上樹葉 T[i]
(可能需要把隱式節點變換成顯式節點)。 - 后綴樹中已經有 T[j..i] 了。
這種情況下,我們什么都不用做。
於是,如果不加任何形式的優化,那么 Ukkonen 的算法就是這么回事:
令 n = |T|; 在 T 尾部加上終結符 $。 for i from 0 to n: // n+1 個階段(包括加上終結符) for j from 0 to i-1: // 每個階段 i 次擴展 從后綴樹的根順着 T[ j..i-1 ] 走到節點 v (顯式/隱式) 如果 v 下方節點中存在第一個字符為 T[i] 的,則什么都不用做。 否則如果 v 是隱式節點,那么: 將 v 改成顯式節點。 在 v 下增加葉節點,從 v 到新節點的邊標記為 T[i]。 若 v 是內部節點或根節點: 在 v 下增加葉節點,從 v 到新節點的邊標記為 T[i]。 否則: (只能是葉節點了): 將 T[i] 加到邊的尾部。
具體的,這里給出構建 MISSISSIPPI$ 的后綴樹的全過程,如下:
6.2 Speed Up!
上面的原生態算法雖通俗易懂老少皆宜,但其時間復雜度相當之高 ( O(|T|3) ), 想要有實用價值必須得提高它的效率 —— 而正如前面提到過的,其實后綴樹可以在 O(|T|) 時間內構造;這個 O(|T|) 的奇妙算法和上面的算法主體流程其實一樣,下面就要介紹加速妙方了,請坐穩。
6.2.1 Stop When T[j..i] Exists
首先說最容易理解的:
很顯然,如果后綴樹 STree(T[0..i-1]) 中已經存在 T[j..i] 了,那么 T[j+1 .. i] 肯定也在其中, 因為 :
- T[0..i-1] 的所有后綴都在后綴樹中。
- T[0..i-1] 的任意子串都可以表示為它的某一個后綴的前綴。
- 所以 T[0..i-1] 的所有子串都在后綴樹中。
- T[j+1 .. i] 是 T[j..i] 的子串, T[j..i] 又是 T[0..i-1] 的子串,所以 T[j+1 .. i] 也是 T[0..i-1] 的子串。
- 所以后綴樹中存在 T[j+1 .. i] 。
於是乎,碰到 T[j..i] 存在的情況,就不必繼續傻乎乎的在樹中查找 T[j+1 .. i-1], T[j+2 .. i-1] ... 了。
6.2.2 Opened Leaves
若知道了字符串的長度 l ,又知道了樹葉的起點 d ,就可以知道這個樹葉的邊的長度為 l - d —— 所以首先,不記錄樹葉的邊長並不影響功能。
而如果直接把所有的樹葉都標記為開放的,做法例如令其長度為無窮大 —— 這樣有一個好處:在對樹葉的擴展中就什么都不用做了。
什么時候可以利用這個小技巧呢?我們看看將 T[i] 加入到 STree(T[0 .. i-1]) 的過程:
-
首先, T[0 .. i-1] 肯定停在樹葉上。
-
假設 STree(T[0 .. i-1]) 有 n 片樹葉,那么 T[1 .. i-1] T[2 .. i-1], T[n-1 .. i-1] 也都會停在樹葉上,因為:
對於任何 j < i-1 如果 T[j .. i-1] 不在樹葉上,那么 T[j+1 .. i-1] 更不可能在樹葉上;
這又是因為, T[j+1 .. i-1] 是 T[j .. i-1] 的后綴,若 T[0 .. i-1] 有子串 S = T[j .. i-1]+'c' (因此 T[j .. i-1] 不在樹葉上),那么 T[0 .. i-1] 肯定也有子串 S[1:] = T[j+1 .. i-1]+'c' (因此 T[j+1 .. i-1] 也不在樹葉上)。
於是如果 STree(T[0 .. i-1]) 有 n 片樹葉,那一定就對應着前 n 個后綴。
所以,已經有了 STree(T[0 .. i-1]) 的話,不必沿着 T[0 .. i-1], T[1 .. i-1] .. T[i-1 .. i-1] 所有這些路徑將 T[i] 加入到后綴樹;而只需要從 n 開始沿着路徑 T[n .. i-1], T[n+1 .. i-1] .. T[i-1 .. i-1] 加入 T[i] 。
6.2.3 Count & Skip
再看從后綴樹的根開始查找路徑 T[j .. i-1] 的過程,不難發現:
某一條邊上進行匹配時,不必一個字符一個字符的比較,而只需要比較第一個字符,跳過剩下來的部分、直接去下一個節點,因為:
- 一個節點每個分支的第一個字符都必定不相等(如果它們相等,則應該被放在上方的邊上)。
- 另一方面,在對 STree(T[0 .. i-1]) 進行擴展,即將 T[i] 加到 T[0 .. i-1] 的每一個后綴后時, T[j .. i-1] (0 <= j <= i-1) 已經在樹中了,因此順着 T[j .. i-1] 查找下一次擴展的位置時,不可能遇到某一段邊實際不匹配但第一個字符相等的情況。
6.2.4 Suffix Link
好了,接下來要講最重要也是最難理解的話題:后綴鏈接了。
6.2.4.1 What is It
用漢語解釋 Suffix link 的話,它就是某種顯式節點之間的鏈接,其目的是簡化順着 T[j .. i-1] 找到下一次擴展的地方的過程。
根據 Count & Skip 的技巧,從根節點開始找到擴展點的算法復雜度已經只與下方的分支數有關了,如果能直接從某個很接近擴展點的顯式節點開始查找,那么顯然能夠提高效率。
很巧的,我們發現,如果有某個內部節點的路徑為 aS,其中 a 是一個字符、 S 是一個(可能為空的)字符串,那么肯定也會有一個路徑為 S 的內部節點、即使沒有,也會在下次擴展的時候產生。
因為: 如果 aS 停在一個內部節點上面,也就是 aS 后有分支,那么當前的 T[0 .. i-1] 肯定有子串 aS+c 以及aS+d ( c 和 d 是不同的兩個字符) 這兩個不同的子串,於是肯定也有 S+c 以及 S+d 兩個子串了。至於“下次擴展時產生”的這種情況,則發生在已經有 aS+c 、 S+c ,剛加入 aS+d (於是產生了新的內部節點),正要加入 S+d 的時候。
這樣,我們將節點 aS 指向 S ,並把這樣的一個指向關系稱為 aS 的 后綴鏈接 。
見圖中紅色箭頭:
6.2.4.2 How It Works
后綴鏈接有什么用呢?
前面似乎提示過了,在 aS 后加上了 d 之后,下一個步驟便是在 S 之后加上 d ;於是同樣,在 aSxy 后加上 z (步驟一)之后,下一個步驟是在 Sxy 后加上 z (步驟二)。如果 aS 停在了內部節點上,那么 S 也停在內部節點上,那么從步驟一到步驟二,通過后綴鏈接,可以直接找到路徑為 S 的這個內部節點;然后通過Count & Skip 技巧,可以直接定位到 xy 后方的擴展點。
參考圖中 A 節點到 B 節點的藍色路徑:
這里用 MISSISSIPPI 的例子來講后綴鏈接似乎不太合適,參考它的構造過程圖示,第九步之前樹中一直只有一個內部節點,第九步沒有利用后綴鏈接加速,而第九步之后,根據 Opened Leaves 技巧,只需要對 P 開頭的那一條邊進行擴展……但這個例子絕對不意味着后綴鏈接沒有用。
嗯,比如說,作為一個正面的案例,構建 MISSISSIPPIMISSIA 的后綴樹的第 17 階段(加入 'A' ),第 13、14、15 次擴展便是順着后綴鏈接進行的( ISSI -> SSI -> SI -> I -> ROOT )。
6.2.4.3 How To Maintain It
那么如何建立和維護后綴鏈接呢?
依然根據上面提到的原理,我們發現:
在某一階段中,前一次擴展訪問到(或者建立)的內部節點到本次擴展訪問到的地方要么已經有了一條后綴鏈接,要么就應該建立一條后綴鏈接。
因此,具體實現起來,我們可以保存一個最后訪問的內部節點的指針 p ,訪問到新的內部節點 node 的時候,將 p 的后綴鏈接指向 node ,再將 p 設為 node 即可。
6.3 Why O(n)
至於為什么這個算法是線性的,分析起來倒是不難:
設 n = |T|。
- 首先,對於每個階段的隱式擴展,花費的都是常數時間,因此整體上是 O(n)。
- 然后,每次顯式擴展都會產生新的樹葉,而整個后綴樹中有 n 個樹葉;因此顯式擴展一共會執行 n 次。 ([2] 中說是 2n 次,為什么?)
- 通過使用 Suffix Link + Count & Skip 技巧,在所有這 n 次顯式擴展中,總共的節點訪問次數為 O(n) 。這又是因為:
- 假設節點 A 的后綴鏈接指向節點 B ,A 的路徑為 pa , B 的路徑為 pb , A 上方有 x 個節點, B 上方有 y 個節點,則有 :
- x <= y + 1 (因為 pb 是 pa 的后綴,因此除了第一個字符之后的分支, pa 路徑上的其他分支 pb路徑上必然也有,反之則不然)。
- 因此,再看每次顯式擴展的過程:從當前位置向下找到擴展點;完成擴展; 向上一個節點;跟隨后綴鏈接來到下一個位置。其中除了“從當前位置向下找到擴展點”這一步之外,剩下的都只需要常數時間。
- 而“向下”查找一次之后,跟隨后綴鏈接來到新的一個節點時最多只能使上方的節點數減少 2。
- 后綴樹整體的深度不超過 n ,而總共又只有 n 次顯式擴展,因此節點的訪問次數為 O(n)
- 因此,顯式擴展整體的時間復雜度也是 O(n)。
- 於是,算法的時間復雜度便是 O(n) 了。
6.4 C++ Code
最后,以下代碼是我的實現,使用 boost license 以及 GPL v3 發布,需要用的話請自由選擇~
使用模板一方面是因為字符的確有不同的類型,另一方面(更主要的)是方便把實現和定義放在同一個文件里,以減少鏈接的工作(我懶嘛),用 C 的童鞋,對不住了。
本人對下面程序的任何 bug 引起的任何后果不負任何責任,但是如果發現有 bug 請您務必與俺聯系。
/*
* File: suffixtree.h
* Author: If
* Created on Sep/14th/2010, 14:02
*
* License: GPL v3 / Boost Software License 1.0
*/
/*
*update: Oct/17th/2010:
Record belongTo information on leaves only,
other works were left to `dfs`, `bfs` and `eachLeaf`
function.
*update: Oct/17th/2010:
Add `eachLeaf` function, for it's quite useful
*/
/*
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
/*
Copyright (C) 2010 If
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* following code may cause permanent brain damage, read at your
own risk. */
#ifndef SUFFIXTREE_H
#define SUFFIXTREE_H
// #define HAS_BOOST
#include <functional>
#include <algorithm>
#include <stdexcept>
#include <iterator>
#include <iostream>
#include <vector>
#include <string>
#include <stack>
#include <queue>
#include <assert.h>
#include <stdio.h>
#ifdef HAS_BOOST
#include <boost/pool/object_pool.hpp>
#endif
using std::vector;
using std::basic_string;
#ifdef DEBUG_STREE
FILE* __streeLogFile=stderr;
#define dbg(...) \
do{ \
fprintf(__streeLogFile, __VA_ARGS__); \
fflush(__streeLogFile); \
} while(0)
#define dbg_puts(str, len) \
do{ \
for(int i=0;i<len;++i) \
fputc(str[i], __streeLogFile); \
fflush(__streeLogFile); \
} while(0)
#else
#define dbg(...) /*nothing at all*/
#define dbg_puts(s, l) /*nothing at all*/
#endif
template <class CharT, class Additional=void*>
struct Node
{
// Important members:
CharT const* str; // since edge-node has one-to-one correspondence
int len; // there is no need to separate them.
int startAt; // for the use of presenting an implicit node.
// it will then be the position of first different
// charactor.
Node * link; // suffix link.
Node * parent;
vector<Node*> branches;
int belongTo; // which string does this leaf belongs to.
// *only recorded on leaves*
// Misc members:
int leafNum; // leaf number ( nth suffix ).
int depth; // no need to explain.
Additional info; // some more information you need, depending on
// your application.
Node():
str(0),
len(0),
startAt(0),
link(0),
parent(0),
branches(),
belongTo(-1),
leafNum(0),
depth(0),
info()
{ }
Node(Node const& other):
str(other.str),
len(other.len),
startAt(other.startAt),
link(other.link),
parent(other.parent),
branches(other.branches),
belongTo(other.belongTo),
leafNum(other.leafNum),
depth(other.depth),
info(other.info)
{ }
~Node() { }
/*
static inline int matchFirst(Node const& node, CharT const val){
return node.str[0]==val? node.len : 0;
}
static inline bool ptrMatchFirst(Node const* node, CharT const val){
return node->str[0]==val? node->len : 0;
}
*/
struct MatchFirst {
CharT val;
MatchFirst(CharT v):
val(v)
{ }
inline bool operator()(Node const* node) const {
return node->str[0]==val;
}
};
};
template <class CharT=char, class T=void*>
class SuffixTree
{
protected:
typedef Node<CharT,T> NodeT;
typedef typename vector<NodeT*>::iterator
NodeIterator;
#ifdef HAS_BOOST
boost::object_pool<NodeT>
allNodes_;
#endif
NodeT* root_;
vector<NodeT*> leaves_;
vector<NodeT*> allLeaves_;
int nLeaves_;
vector<basic_string<CharT> >
strings_;
NodeT* latestInternalNode_;
NodeT* currentPos_;
int phase_;
int extension_;
protected:
NodeT* allocNode() {
#ifdef HAS_BOOST
return allNodes_.malloc();
#else
return new NodeT;
#endif
}
void freeNode(NodeT *& node) {
#ifdef HAS_BOOST
allNodes_.free(node);
#else
delete node;
#endif
node = 0;
}
/**
* following str[0:len], and then add str[len] to current tree.
*
* @return true if undone, false if str[0..len] already exists - which means
* extension process is done.
*/
bool extend (CharT const* str, int pos);
// follow path "str" from node down.
NodeT* jumpDown(CharT const* str, int len, NodeT* node, bool fromFront=0);
/**
* split one edge from its startAt position.
*
* @param edge: a node with startAt!=0
* @param leafLabel: label of the newly created leaf, with length==1
* @return newly created leaf.
*/
NodeT* splitEdge(NodeT* edge, CharT const* leafLabel);
void putEnd() {
int p = phase_+1;
int const nth = strings_.size()-1;
for(NodeIterator itr=leaves_.begin(),end=leaves_.end(); itr!=end; ++itr)
{
(*itr)->len = --p - (*itr)->depth;
allLeaves_.push_back(*itr);
}
}
#ifndef HAS_BOOST
inline void destroy(NodeT* node) {
for(NodeIterator itr=node->branches.begin(), end=node->branches.end();
itr!=end; ++itr)
destroy(*itr);
freeNode(node);
}
#endif
private:
SuffixTree(SuffixTree<CharT,T> const&); // complicated while useless,
// i've got no interest.
public:
inline SuffixTree();
inline SuffixTree(basic_string<CharT> const&);
#ifdef HAS_BOOST
~SuffixTree() { /*nothing to do*/ }
#else
~SuffixTree() { destroy(root_); }
#endif
void addString(CharT const * str, int len);
void addString(basic_string<CharT> const& str) {
addString(str.c_str(), str.length()); // to maintain a copy
}
NodeT const* root() {
return root_;
}
/*
void deleteString(CharT const * str, int len);
void deleteString(basic_string<CharT> const& str) {
deleteString(str.c_str(), str.length());
}
*/
// will call f(node) on each node of this tree, including root.
template <class Function>
void dfs(Function f) {
std::stack<NodeT*> todo;
todo.push(root_);
while(!todo.empty()) {
NodeT* node = todo.top();
todo.pop();
for(NodeIterator itr=node->branches.begin(),
end=node->branches.end();
itr!=end;
++itr) {
todo.push(*itr);
}
f(*node);
}
}
// will call f(node) on each node of this tree, including root.
template <class Function>
void bfs(Function f) {
std::queue<NodeT*> todo;
todo.push(root_);
while(!todo.empty()) {
NodeT* node = todo.front();
todo.pop();
for(NodeIterator itr=node->branches.begin(),
end=node->branches.end();
itr!=end;
++itr) {
todo.push(*itr);
}
f(*node);
}
}
template <class Function>
void eachLeaf(Function f) {
for(NodeIterator itr=allLeaves_.begin(), end=allLeaves_.end();
itr!=end; ++itr) {
f(**itr);
}
}
// human-readable json-like format output
void print(std::basic_ostream<CharT> * stream);
// machine-readable dot format output, can be used to
// generate graph using Graphviz
void dot(std::basic_ostream<CharT> * stream);
};
template <class CharT, class T>
SuffixTree<CharT,T>::SuffixTree():
root_(0),
leaves_(),
nLeaves_(0),
strings_(),
latestInternalNode_(0),
currentPos_(0),
phase_(0),
extension_(0)
{
root_ = allocNode();
root_->parent = root_; // for convenience
root_->link = root_;
root_->len = 0;
root_->str = 0;
root_->startAt = 0;
currentPos_ = root_;
}
template <class CharT, class T>
SuffixTree<CharT,T>::SuffixTree(basic_string<CharT> const& str):
root_(0),
leaves_(),
nLeaves_(0),
strings_(),
latestInternalNode_(0),
currentPos_(0),
phase_(0),
extension_(0)
{
root_ = allocNode();
root_->parent = root_; // for convenience
root_->link = root_;
root_->len = 0;
root_->str = 0;
root_->startAt = 0;
currentPos_ = root_;
this->addString(str);
}
template <class CharT, class T>
void SuffixTree<CharT,T>::addString(CharT const* str, int len)
{
strings_.push_back(basic_string<CharT>(str, str+len));
int whichString = strings_.size()-1;
CharT const* cstr = strings_.back().c_str();
dbg("adding string \"%s\" to suffix tree ... \n", cstr);
root_->len = 0;
root_->depth = 0;
leaves_ = vector<NodeT*>();
nLeaves_=0;
currentPos_=root_;
for(phase_=0; phase_<=len; ++phase_) { // there's a '\0' behind
bool undone = true;
latestInternalNode_ = 0;
for(int j=nLeaves_; j <= phase_ && undone; ++j) {
extension_ = j;
undone = extend( cstr+extension_, phase_-extension_ );
}
}
putEnd();
}
template <class CharT, class T>
typename SuffixTree<CharT,T>::NodeT*
SuffixTree<CharT,T>::
jumpDown(CharT const* str, int len, NodeT* node, bool fromFront)
{
dbg("# jumping down path len=%d str=\"", len);
dbg_puts(str,len);
dbg("\" from node %08x ... \n", node);
int i=0;
if(fromFront) {
if (node!=root_ && str[0]!=node->str[0]) {
dbg("!!! cannot find such path !!!\n");
throw(std::runtime_error("path not found"));
}
i = node->len != -1?
node->len:
phase_ - node->depth;
}
for(; i<len; ) {
NodeIterator itr =
std::find_if( node->branches.begin(),
node->branches.end(),
typename NodeT::MatchFirst(str[i]) );
if(itr == node->branches.end()) {
dbg("!!! cannot find such path !!!\n");
throw(std::runtime_error("path not found"));
} else {
dbg("> going into node %08x which begins with \'%c\', len=%d,"
" depth=%d\n",
*itr, (*itr)->str[0], (*itr)->len, (*itr)->depth );
node = *itr;
if( node->len != -1 ) // internal node
i += node->len;
else // leaf
i += phase_ - node->depth;
}
}
if( i != len ) { // implicit node
if(node->len != -1)
node->startAt = node->len - (i-len);
else
node->startAt = phase_ - node->depth - (i-len);
}
dbg("< done.\n");
return node;
}
template <class CharT, class T>
typename SuffixTree<CharT,T>::NodeT*
SuffixTree<CharT,T>::splitEdge(NodeT* node, CharT const* leafLabel /*len==-1*/)
{
/*
*
* \
* # <-- startAt
* \
* node
*
* => => => => => => =>
*
* \
* \
* newInternal
* / \
* / newLeaf (return value)
* node
*
*/
dbg("X spliting edge %08x \n", node);
NodeT* newLeaf = allocNode();
NodeT* newInternal = allocNode();
// newInternal->belongTo = node->belongTo;
newInternal->startAt = 0;
newInternal->len = node->startAt;
newInternal->str = node->str;
newInternal->parent = node->parent;
newInternal->link = root_;
newInternal->depth = node->depth;
newInternal->branches.push_back(node);
newInternal->branches.push_back(newLeaf);
*(std::find(newInternal->parent->branches.begin(),
newInternal->parent->branches.end(),
node)) = newInternal;
node->depth += node->startAt;
node->str += node->startAt;
node->parent = newInternal;
if(node->len != -1) {
node->len -= node->startAt;
}
node->startAt = 0;
newLeaf->str = leafLabel;
newLeaf->depth = node->depth;
newLeaf->link = root_;
newLeaf->len = -1;
newLeaf->parent = newInternal;
dbg(" new internal node = %08x ; \t new leaf = %08x\n",
newInternal, newLeaf);
return newLeaf;
}
template <class CharT, class T>
bool
SuffixTree<CharT,T>::extend(CharT const* str, int pos)
{
dbg("{{{ in phase %d, extension %d;\n", phase_, extension_);
dbg(" adding \'%c\' to suffix tree ... \n", str[pos]);
int const whichString = strings_.size()-1;
int skiped = currentPos_->depth;
NodeT *node = jumpDown(str+skiped, pos-skiped, currentPos_, true);
if (node->startAt != 0) { // implicit node
dbg(" got implicit node %08x starts at %d\n", node, node->startAt);
if(node->str[node->startAt] != str[pos]) {
NodeT *newLeaf = splitEdge( node, str+pos );
newLeaf->leafNum = extension_;
newLeaf->belongTo=whichString;
node = newLeaf->parent;
if(latestInternalNode_) {
latestInternalNode_->link = node;
dbg("-> creating suffix link from %08x to %08x\n",
latestInternalNode_,
node);
}
latestInternalNode_ = node;
currentPos_ = node->parent;
leaves_.push_back(newLeaf);
++nLeaves_;
} else {
dbg(" suffix already exists.\n}}} done.\n");
node->startAt = 0;
return false; // suffix already exists, all done.
}
} else { // explicit node
dbg(" got explicit node\n");
if(latestInternalNode_) {
latestInternalNode_->link = node;
dbg("-> creating suffix link from %08x to %08x\n",
latestInternalNode_,
node);
}
latestInternalNode_ = node;
if( std::find_if( node->branches.begin(),
node->branches.end(),
typename NodeT::MatchFirst(str[pos]) )
!= node->branches.end()) {
dbg(" suffix already exists.\n}}} done.\n");
currentPos_ = node;
return false; // all done.
} else if(node->branches.empty() && node!=root_) {
// a leaf created in the past, now it should be exdended.
node->belongTo=whichString;
node->str = str+pos-node->len;
node->len = -1;
currentPos_ = node->parent;
node->leafNum = extension_;
leaves_.push_back(node);
++nLeaves_;
} else {
// add a leaf to existing explicit node.
NodeT *newLeaf = allocNode();
dbg(" adding new leaf %08x to node %08x.\n", newLeaf, node);
newLeaf->parent = node;
newLeaf->link = root_;
newLeaf->str = str+pos;
newLeaf->len = -1; // leaf
newLeaf->belongTo=whichString;
newLeaf->depth = node->depth + node->len;
node->branches.push_back(newLeaf);
currentPos_ = node;
newLeaf->leafNum = extension_;
leaves_.push_back(newLeaf);
++nLeaves_;
}
}
dbg("-> following suffix link from %08x to %08x ...\n",
currentPos_,
currentPos_->link );
currentPos_ = currentPos_->link;
dbg("}}} done.\n");
return true;
}
static inline char* x08 (char* buf, void const * ptr) {
sprintf(buf, "%08x", ptr);
return buf;
}
template <class CharT, class T>
static void __printSuffixTreeNode(Node<CharT,T>* node,
int margin_left,
int padding_left,
std::basic_ostream<CharT> * stream)
{
int i,I;
char addr[16];
if( node->branches.empty()) {
for(i=0; i<margin_left; ++i)
*stream<<' ';
*stream << '\"';
for(i=0; i<node->len; ++i)
*stream<< node->str[i];
*stream << "\" " << x08(addr, node)
<< "[" << addr << "] ";
*stream << "\n";
} else {
for(i=0; i<margin_left; ++i)
*stream<<' ';
*stream << '\"';
for(i=0; i<node->len; ++i)
*stream<< node->str[i];
*stream << "\" " << x08(addr, node)
<< "[" << addr << "] ";
*stream << ": {\n";
for(i=0, I=node->branches.size(); i<I; ++i) {
__printSuffixTreeNode(node->branches[i],
margin_left+padding_left,
padding_left, stream);
}
for(i=0; i<margin_left; ++i)
*stream<<' ';
*stream << "} \n";
}
}
template <class CharT, class T>
void SuffixTree<CharT,T>::print(std::basic_ostream<CharT> * stream)
{
assert(!"function not supported");
}
template <>
void SuffixTree<char,void*>::print(std::ostream * stream)
// ||
// std::basic_ostream<char>
{
*stream<<"Root ";
__printSuffixTreeNode<char,void*>(root_, 0,2, stream);
}
template <class T>
static void __dotPrintNode(Node<char,T>const*node,std::ostream*stream)
{
char buf[16];
int i,I,j;
for(i=0,I=node->branches.size(); i<I; ++i) {
Node<char,T> const* p = node->branches[i];
*stream<<"node"<<x08(buf,p)<<" [label=\"\"";
if(p->branches.empty())
*stream<<" peripheries=2 width=0.1 height=0.1";
*stream<<"];\nnode"<<x08(buf,node);
*stream<<" -> node"<<x08(buf,p)
<<" [label=\"";
for(j=0;j<p->len;++j)
if(p->str[j])
*stream<<p->str[j];
else
*stream<<"$";
*stream<<"\"];\n";
__dotPrintNode(p,stream);
}
if(!node->branches.empty()) {
*stream<<"node"<<x08(buf,node);
*stream<<" -> node"<<x08(buf, (node->link))<<" "
<<"[color=red];\n";
}
}
template <class CharT, class T>
void SuffixTree<CharT,T>::dot(std::basic_ostream<CharT> *) {
assert(!"function not supported");
}
template <>
void SuffixTree<char,void*>::dot(std::ostream * stream)
{
char buf[16];
*stream<<"digraph stree {\n";
*stream<<"node"<<x08(buf, root_)<<" [label=\"root\"];\n";
*stream<<"node"<<buf<<" -> node"<<buf<<";\n";
*stream<<"node [width=0.2 height=0.2] \n";
__dotPrintNode<void*>(root_, stream);
*stream<<"}\n";
}
#endif /* SUFFIXTREE_H */
// vi:ts=4:foldmethod=marker
7 References
[1] | : Esko Ukkonen, Suffix tree and suffix array techniques for pattern analysis in strings, Erice School 30 Oct 2005 |
[2] | (1, 2, 3, 4) : Gusfield, Dan (1999) [1997]. Algorithms on Strings, Trees and Sequences: Computer Science and Computational Biology. USA: Cambridge University Press. ISBN 0-521-58519-8 |
[3] | (1, 2) : E. Ukkonen, On-Line Construction of Suffix Trees, Algorithmica, 14 (1995), 249-260 |
Licensed under CC Attribution-NonCommercial-NoDerivs 3.0 Unported License
Author: If
轉載請注明:來自 IF's
本文地址: http://www.if-yu.info/2010/10/3/suffix-tree.html
Trackback:http://www.if-yu.info/2010/10/3/suffix-tree.html?code=aghpYW15dWd1b3INCxIFRW50cnkYwaYIDA