Ukkonen後綴樹算法的真·清晰解釋

本站有個翻譯的文章,名字叫Ukkonen 的後綴樹算法的清晰解釋。這篇文章寫得不錯,可是仍是犯了錯誤的。 算法

我按照這篇文章的說明實現了所謂的Ukkonen算法,可是在測試時出現了錯誤,過後,進行了大量的排查(由於我一開始認爲確定不是文章的問題是個人問題,花了2天沒解決,最後拋開「文章正確」的觀點才解決) 測試

下面,是我從新整理後的Ukkonen算法,採用的例子仍是"abcabxabcd"。 spa

生成後綴數和繪圖的C++源碼在http://www.oschina.net/code/snippet_593413_38384 .net

一開始依次插入'a'、'b'、'c'三個字符時,後綴樹以下: 翻譯

指針

圖中,綠色的節點表示活動節點;節點內填充黃色,表示爲根節點;沒有活動邊,說明活動長度爲零。 code

圖中的黑色箭頭表示的邊,x:(n,#)格式的,表示一個指向葉節點的邊,x爲該邊第一個字符,n爲該邊起始的位置。正常的邊會列出其包含的全部字符。 索引

葉節點的值,表示該葉節點表明的後綴的起始位置。 ip

從上面的例子說明,當向一個活動長度爲零的活動點插入已有的邊(的首字符)不存在的字符時,會插入新的邊和葉節點。剩餘後綴數會在掃描前+1,插入後-1,所以保持爲0。 字符串

下一步,插入'a'(當前掃描位置3):

由於字符'a'已經存在於某個邊中,因而咱們就將活動三元組置爲(root,'a',1),剩餘後綴數,並完成本次字符的掃描,事實上,活動邊不是表示爲'a',而是3——這表示活動邊是以文本的索引爲3的字符起始的。即,活動三元組爲(root,3,1)。剩餘後綴數在掃描前+1,而本次沒有插入後綴,所以剩餘後綴數爲1。

圖中,綠色箭頭表示一個活動邊,而該邊最後冒號後面的數字表示活動長度。

下一步,插入'b'(當前掃描位置4)

由於字符'b'也已經存在於活動邊的下一個位置,因而咱們就將活動三元組置爲(root,3,2),剩餘後綴數爲2,並完成本次字符的掃描。

下一步,插入'x'(當前掃描位置5)

要插入'x',當前邊爲"abcabx",而活動長度爲2,該邊下一個字符是'c'不是'x',所以咱們將該邊分割開,也就是圖中的ab邊和c:(2,#)邊及二者間的節點,而且向該節點添加一個新的邊x:(5,#)和葉節點3。

爲啥葉節點是3?由於當先後綴數爲3,表示咱們插入的後綴是"abx",在該後綴前面的部分是"abc",由於"ab"是隱含在已有後綴樹裏的,實際插入的邊是從'x'開始的,可是葉節點倒是從第二個'a'開始的,其索引爲3。簡化計算的話,就是:當前掃描位置+1-剩餘後綴數。

在插入新的邊以後,剩餘後綴樹-1;由於剩餘後綴數>0,咱們要重複插入後綴的操做,直到剩餘後綴數==0或者遇到該後綴被隱含的狀況。

在插入新的邊和節點後,須要更新活動三元組。由於活動節點是根節點,操做爲:活動邊(索引)+1,活動長度-1,所以新的三元組爲(root,4,1)。

文本索引4的位置,字符爲'b',據此,咱們肯定新的邊爲b:(1,#)。

由於循環,咱們將該邊分割、插入了新的邊x:(5,#)和葉節點4。

同時,根據規則,咱們要添加後綴指針,後綴指針會添加到掃描一次字符的過程當中,由於分割邊出現的新的內部節點之間(從舊到新)。假設一次掃描中,因分割出現的新節點依次爲a,b,c;則應添加後綴指針,a -> b和b -> c。

圖中,後綴指針用紅色箭頭表示。

此時,更新三元組爲(root,5,0),而剩餘後綴數爲1。

下一次插入操做,和插入索引一、二、3位置的字符時同樣的規則,在根節點(活動節點)添加了新的邊x:(5,#)和葉節點5。

剩餘後綴數爲0,本次掃描結束。

下一步,插入'a'(當前掃描位置6)


更新三元組爲(root,6,1),剩餘後綴數1

下一步,插入'b'(當前掃描位置7)


更新三元組爲(root,6,2),剩餘後綴數2;由於抵達了新的點,三元組重置爲(green,6,0)

下一步,插入'c'(當前掃描位置8)


更新三元組爲(green,8,1),剩餘後綴數3;

下一步,插入'd'(當前掃描位置9)


進行了一系列的後綴插入:

剩餘後綴數+1(如今是4);

分割c:(2,#)=>0邊,而後由於活動點有後綴指針,所以活動點重置爲該點(root經邊b到達該點),而活動邊和活動長度保持爲8和1

分割c:(2,#)=>1邊,生成新的後綴指針。此時,活動點沒有後綴指針了。活動點也不是根節點。爲了找到下一個活動點,有兩種方法。

1)總而言之,咱們知道剩餘後綴數和當前掃描位置,換句話說,咱們知道當前要插入的後綴,所以從根節點沿着該後綴查找就是了。

當前掃描位置9,剩餘後綴數2,所以當前要插入的後綴從8起始,插入的是[8,9]即"cd",重置活動三元組爲(root,8,1):即根節點、當前掃描位置-剩餘後綴數+1,剩餘後綴數-1

而後對這個三元組進行修正,也就是沿着後綴樹走,直到活動長度爲0或者小於活動邊的長度。

所以,咱們找到了root經邊c到達的節點。

2)將活動三元組更新爲(當前活動點,當前掃描位置-當前活動長度,當前活動長度)

從該活動點向父節點走,每次移動到父節點,都要讓活動邊(索引)減去通過的邊的長度,而當前活動長度加上通過的邊的長度

直到根節點,此時的操做和活動節點原本就在根節點是同樣的:活動邊(索引)+1,活動長度-1

或者移動到的節點有後綴指針,那麼,咱們沿着後綴指針移動一次活動節點,活動邊和活動長度不變

到達根節點或者沿後綴指針移動後,也要進行修正,沿着後綴樹走,直到活動長度爲0或者小於活動邊的長度。

這兩種方法無論採用哪種,都會到達同一個點。通常說,樹較小時,第一種更簡單,而樹比較複雜的時候,第二種更好。

分割c:(2,#)=>2邊,生成新的後綴指針。活動三元組更新爲(root,9,0),剩餘後綴數1

插入新的邊d:(9,#)和新的點9

本次掃描結束。

…………

這個過程能夠一直持續下去,知道接受了一個終止標識符,或者進行結束操做。


由於掃描'd'後,剩餘後綴數已經減小到0了,咱們的結束操做只是將根節點設置爲一個後綴標識節點,表明空後綴。

事實上,由於'd'在字符串中僅在最後出現了一次,它的行爲和掃描終止標識符是相同的。若是咱們並不真的插入終止標識符(甚至咱們都不用去比較終止標識符和活動位置下一個字符),將每一次添加僅含終止標識符的邊和葉節點的操做替換爲修改節點的屬性,那麼就是一個標準的結束操做了。

總的來講,咱們只作了一個修改,那就是活動三元組的更新規則。

確切地說,是活動點不是根節點且沒有後綴指針的時候的更新規則。

本文中的圖都是用graphviz生成的。

相關文章
相關標籤/搜索