1. 程式人生 > >字尾樹系列二:線性時間內構建字尾樹(包含程式碼實現)

字尾樹系列二:線性時間內構建字尾樹(包含程式碼實現)

  上一篇文章已經介紹了字尾樹的前前後後的知識,並且採用各種技巧逼近線性時間了,至於具體怎麼操作大家看完之後應該多多少少有點想法了。而之所以將本文跟上一篇文章分開,主要考慮有三:

  • 第一,和在一起文章就會太長了,看的頭疼。
  • 第二,理論跟實現本來就有差異,本文中一些具體實現並沒有嚴格遵守上文中的條條框框。當然了,主題思想是一樣的。
  • 第三,本文會從具體實現的角度,在實現過程中進一步闡述上文中的原理。換個角度看問題,會加深理解。

  宣告一下,原本我打算自己操刀來寫,但是我在網路上搜索到一篇很好的資源,本文也基本上沿著它的思路來寫,建議英文不太差的小夥伴們自己先看看

原文。當然嘍,我也會把本文跟前一篇文章結合起來一起闡述,讓大家體味到理論跟實現是如何水乳交融的。

     上文中提到我們最終只是需要沿著去除頭尾之後的“尾部連結串列”以及根節點更新增加節點就好了。但是我們怎麼來做呢?帶著這個問題,正式進入我們的實現環節!

     首先引入兩個概念:

    • 當前活躍點active point:這是一個三元組包含了三個資訊(活躍點,活躍邊,活躍半徑)。初始值為(root,’\0x’,0),代表活躍點為根節點,沒有活躍邊,活躍長度為0;
    • 計數器remainder
      : 記錄我們在當前階段需要插入的葉子節點數目。注意到我們每次插入都會增加新的葉子節點,具體原因見上一篇文章。初始值為1,每次進入新階段增加1,每次從活躍點新增一個葉子節點減少1。

  我們每一階段的目標都是把當前階段的所有後綴插入到字尾樹中。至於葉子節點,我們跟上文所說的一樣,採用   [ , #]格式,每到一個新的階段自動更新。

 

  好了,下面給出構建字尾樹的三個定理:

  規則一(活躍點為根節點時候的插入):

    • 插入葉子之後,活躍節點依舊為根節點;
    • 活躍邊更新為我們接下來要更新的字尾的首字母;
    • 活躍半徑減1;

  規則二(字尾連結串列):

    • 每個階段,當我們建立新的內部節點並且不是該階段第一次建立內部節點的時候,我們需要用指標從當前內部節點指向本階段最近一次建立的內部節點

規則三(活躍點不為根節點時候的插入):

    • 如果當前活躍點不是根節點,那麼我們每次從活躍點新增一個葉子之後,就要沿著字尾連結串列到達新的點,並更新活躍節點;如果不存在後綴連結串列了,我們就轉移到根節點,將活躍節點更新為根節點。活躍半徑以及活躍邊不變。

 

      是不是感覺這裡面的“字尾連結串列”跟我們之前談到的尾部連結串列非常相似啊,而且這個活躍點的概念是不是跟之前的尾部連結串列也有千絲萬縷的關係呢,哈哈。留這些疑問!待會兒再說。我們先來通過一個例子來具體體驗一把怎麼建立字尾樹。例子來自上文中提到的那篇原文。

  我們為字串abcabxabcd建立字尾樹吧。

  一開始活躍點三元組為(root,’\0’,0),計數器remainder為1;

 

第一階段插入字元a,#=1。

  我們從當前活躍節點(根節點)出發看看有沒有那個邊的第 0(活躍半徑) 個字母是a,發現沒有,於是按照規則一插入葉子節點。活躍邊更新為接下來要插入的字尾首字母,但是由於已經沒有需要插入的了,故而活躍邊也不變便;由於活躍半徑已經是0,故而不用減1了;計數器remainder減少1變成0。我們獲得了這樣一棵樹

clip_image001

  因為當前#=1,故而上圖的這個字尾樹就是下圖(注意一下,區間[0,1]的含義是第一個字元,這點跟之前的表示方法不太一樣)

clip_image002

  此時三元組為(root,’\0’,0),remainder為0;

 

第二階段,插入字元b,#=2; 三元組不變仍為(root,’\0’,0),remainder增加1,等於1;

  我們從當前活躍節點(根節點)出發看看有沒有哪個邊的第 0(活躍半徑) 個字母是b,發現沒有,於是按照規則一插入葉子節點。情況同上,三元組為(root,’\0’,0),remainder為0;我們獲得了這樣一棵樹:

clip_image003clip_image004

  因為#=2,故而左圖等同於右圖。

 

第三階段,插入字元c;#=3;三元組不變仍為(root,’\0’,0),remainder增加1,等於1;

  跟第二階段類似,三元組為(root,’\0’,0),remainder為0;我們獲得了這樣一棵樹:

clip_image005clip_image006

  因為#=3,故而左圖等同於右圖。

 

第四階段,插入字元a; #=4; 三元組為(root,’\0’,0),remainder增加1,等於1;

  這時,我們從當前活躍節點(根節點)出發看看有沒有哪個邊的第 0(活躍半徑) 個字母是a。結果發現了!!!那麼此時,我們更新三元組為(root, ‘a’, 1), remainder不變。更新完係數之後,我們就進入下一階段了。

  為什麼?還記得上一篇文章中,”當新字尾出現在原先後綴樹中”是怎麼辦麼?哈哈,就是更新系數後不管了。

  我們獲得了這樣一棵樹:

clip_image005[1]clip_image007

  看出來了吧,樹的形狀沒有變,但是葉子節點的邊自動更新了。這也就是上一篇文章中提到了自動更新葉節點。按上文的說法這裡應該有尾部連結串列從第一個點(邊為abca)指向第二個點(邊為bca),第二個點指向第三個點(邊為ca),第三個點指向根節點,但是呢,顯然這個連結串列上除了根節點都是葉節點,沒有儲存的必要啊。所以這裡沒有看到“尾部連結串列”的痕跡。這也是一種優化,但是,後面你會見到它的。哈哈,所謂神龍見首不見尾!我們接著來看。

 

第五階段,插入字元b;#=5; 三元組為(root,’a’,1),remainder增加1,等於2;

      這時,我們從當前活躍點(根節點)出發看看邊首字母為a(活躍邊)的第1(活躍半徑)個字母是b。結果又發現了!!

  注意一下,之前因為沒有明確的活躍邊,故而可以隨便找邊,但是確定了活躍邊之後就只能沿著活躍邊來找了。

  因為這一次我們需要插入的字尾除了b之外,還有上次剩餘的,故而我們需要插入的字尾是ab ,b。活躍邊不變,只是活躍半徑需要增加1,等於2。於是三元組變成了(root,’a’,2),計數器remainder不變,仍然為2。

  我們獲得了這樣一棵樹:

clip_image008

 

第六階段,插入字元x; #=6; 三元組為(root,’a’,2), remainder增加1,等於3;我們需要插入的字尾有abx,bx,x

  這時,我們從當前活躍點(根節點)出發看看沿著活躍邊(邊首字母為a)的第2(活躍半徑)個字母是不是x。結果沒發現!

  於是我們按照規則一,從當前生長點(活躍點沿著活躍邊走活躍半徑個字元)增加一個葉子,插入x,也就是代表了abx。

     獲得了這個樹:

clip_image010

  插入完abx之後我們需要插入bx了,但是我們需要先調整三元組。活躍邊首字母就應該變成b了,活躍半徑減少1,於是我們的三元組變成了(root,’b’,1),remainder減少1,等於2。根據規則一,我們再從當前活躍點(根節點)出發看看沿著活躍邊(邊首字母為b)的第1(活躍半徑)個字母是不是x。結果不是!於是再按照規則一,從當前生長點(活躍點沿著活躍邊走活躍半徑個字元)增加一個葉子,插入x,也就代表了bx。

  我們獲得了這個樹:

clip_image011

  但是還沒完,因為這已經是我們在此階段建立的第二個內部節點了,根據規則二,故而我們需要建立”字尾連結串列”。此時三元組變成(root,’x’,0), remainder減少1,等於1。

clip_image012

  接下來就要插入x了。此時活躍半徑已經是0了,於是活躍邊一定是空的了,我們檢視根節點所有邊中首字母有沒有是x的,結果沒有,於是插入x。從當前生長點(此時因為活躍半徑是0,活躍點就是生長點)增加葉子,插入x。得到下面這顆樹。根據規則一,此時的三元組變成了(root, ‘\0’, 0),remainder減少1,等於0。此階段順利結束了。

clip_image013

 

第七階段,插入字元a;#=7;三元組為(root, ‘\0’, 0),remainder增加1,等於1。需要增加的字尾只有一個就是a;

      還是一樣,鑑於活躍半徑為0,我們從看看根節點有那個邊的首字母為a,結果發現了。於是我們調整三元組為(root,’a’,1)。結束

 

第八階段,插入字元b;#=8;三元組為(root, ‘a’, 1),remainder增加1,等於2。需要增加的字尾有兩個ab,b;

     我們先從當前活躍節點(根節點)出發,沿著活躍邊的第1(活躍半徑)個字元是不是b,結果是!於是我們調整三元組為(root, ‘a’, 2)。

  注意了,我們已經來到了一個內部節點而不再跟以前一樣停留在邊上,於是我們再次調整三元組(類似於初始化)為(node1,’\0’,0)。這裡,node1代表了上圖中從根節點出發經過邊ab,到達的那個點。

 

第九階段,插入字元c;#=9;三元組為(node1,’\0’,0),remainder增加1,等於3。需要增加的字尾有三個abc,bc,c;

   鑑於活躍半徑為0,我們直接看看當前活躍點(也就是node1)的邊有沒有以c為首字母的。結果有,於是更新三元組(node1,’c’,1)。注意一下雖然此時應該最先插入字尾abc,但是由於活躍點已經變化了,我們以當前活躍點為基準,故而活躍邊以’c’標誌。

 

第十階段,插入字元s;;#=10;三元組為(node1,’c’,1),remainder增加1,等於4。需要增加的字尾有四個abcd,bcd,cd,d;

       我們從當前活躍點node1出發沿著活躍邊c,第1個字元是否是d,發現不是!於是在當前生長點(活躍點沿著活躍邊前進活躍半徑個字元)增加一個葉子,得到下圖的樹。

clip_image014

  由於當前活躍點不是根節點,所以按照規則三,我們沿著字尾連結串列前進,更新三元組為(node2,’c’,1),remainder減少1,等於3。node2為下圖中的紅色點。

clip_image015

  從node2出發沿著活躍邊c出發,第1個字元是不是d,發現不是,於是在當前生長點增加葉子。注意此時要按照規則二建立新的字尾連結串列了。

clip_image016

 

  按照規則三,沿著字尾連結串列,由於已經沒有下一個節點了,於是跳回到根節點,更新三元組(root,’c’,1)。remainder減少1,等於2.

      從根節點出發沿著活躍邊c,第1個字元是不是d?發現不是,於是在當前生長點增加葉子,注意仍按按照規則二建立字尾連結串列

clip_image018

  之後按照規則一更新三元組,活躍點不變,當前帶插入的字尾是d,故而修改活躍邊為d,活躍半徑減少1變成0。remainder減少1,等於1。

  現在插入字尾d,鑑於活躍半徑為0,看看根節點有沒有邊是以d為首字母的,發現沒有!於是增加葉子。

clip_image019

  至此,全都結束了!字尾樹建立完畢

 

  看完上面的建樹過程,有點暈吧,哈哈,其實我也是。現在來說說上述演算法跟上一篇文章提到的方法之間的關聯吧。

  上一篇文章中我們提到的方法是每次沿著優化後的尾部連結串列更新,增加葉節點。其實我們這麼來看,整個尾部連結串列是由三部分組成的,第一部分是葉子節點;第二部分是“除根節點外會增加葉子的節點”;第三部分是會導致 新字尾出現在原先後綴樹中 的節點,這部分節點對樹結構沒有影響;所謂優化後的尾部連結串列是指去除第一部分和第三部分。當然嘍第二部分第三部分都可能是空的。這樣一來其實優化後的尾部連結串列由兩部分組成:“尾部連結串列”的一部分(在本演算法中就是字尾連結串列)。但是理論上這麼說就可以了,實際上怎麼來實現呢?

  我們看一下,因為”一朝為葉,終身為葉”,而每次遍歷字尾連結串列的時候都會增加一個葉子,而最終的字尾樹也只有|T|個葉子,所以我們一共需要遍歷字尾連結串列|T|次。顯然了,字尾連結串列是很“少”的。故而我們再去保留尾部連結串列,然後去定位第一個非葉子節點就很划不來了。於是,尾部連結串列作為一個理解上面的工具,在實踐中(上述演算法)就不維護啦,我們直接採用字尾連結串列。

  於是乎,我們回過頭來看看上述演算法。

  remainder是計數器,表明到當前階段還需插入幾個字尾,每個階段增加1,是代表了字尾–從當前將要被插入的字元開始到原始串尾的這個字尾。當也葉子被插入的時候,rremainder減少1。

  首先初始化的時候活躍點是根節點,活躍半徑是0。於是我們開始準備插入一個字元,如果字元沒在根節點的任何一條邊(預設為連結各個兒子的邊)中出現,那麼表明什麼意思?表明當前生長點(根節點,所謂生長點就是準備長葉子的點),就是我們“尾部列表中”第二部分的節點,而remainder為1,代表當前只需插入一個葉子,於是插入即可,remainder減少1;如果字元在根節點的某一條邊中出現,那麼這是什麼意思?這表明當前生長點(根節點),是我們第三部分的點,於是我們調整一下當前生長點的位置,這是為什麼呢?因為,假設當前要插入的字元是a,下一次要插入b,那麼我們這一階段沒插入葉子,下一階段多了一個“欠債“,就要還要插入ab。那麼我們將生長點挪到樹中字元a的後面,就意味著我們下一階段只需要在生長點後面插入字元b就可以了,一步到位,方便快捷。鑑於本階段沒有插葉子,所以remainder不變。調整三元組也是為了”挪動生長點“。

  下一次,我們要插入字元b的時候,我們在當前生長點後面找一找有沒有b(其實就是為了驗證當前生長點是不是字尾連結串列中的第三部分的點),如果沒有,意味著是第二部分的點(因為肯定不是葉節點,又不是第三部分的點),那麼就插入葉節點吧,remainder減少1,調整活躍邊為下一個需要插入字尾的首字母,也就是b,活躍半徑減少1。這也是為了調整生長點(快速找到下一個插入葉子的點)。於是看看根節點哪個邊首字母為b,如果沒有就插入新節點,如果有就調整生長點(這一塊跟前面類似,不再多說)。我們說說另一種情況,就是我們在生長點(root,’a’,1)之後還找到了b,這說明當前生長點是第三部分的點,那麼怎麼辦呢,繼續調整生長點為(root,’a’,2)。

  假設上一次之後,調整生長點為(root,’a’,2),這一次再插入字元c,remainder增加1,等於3。如果當前生長點後沒有c,意味著當前生長點是第二部分的點,於是插入葉子c就好了,代表著”欠債“字尾abc。remaidner減少1,調整生長點為(root,’b’,1),為什麼如此確定根節點有某條邊是以b為首字母呢?原因是這樣的,你看啊,我們活躍半徑是2,必然是因為我們剛剛插入的那個字尾abc的前兩個字元出現在後綴樹中,既然ab出現在了字尾樹中,那麼依據前文中的原理,b也一定出現在後綴樹中。大家發現沒有,這個調整生長點的過程是不是太迅速啦。

  事實上來說我們就憑藉上述的這個過程似乎已經建立字尾樹了,但是我們仔細看看,如果定位生長點的時候的這個(root,’a’,2)中的活躍半徑不是2,而是一個很大的數字,比如100,那麼就算我們知道下一個生長點是(root,’b’,99),真的能定位到準確的生長點麼??不一定吧,根節點沿著一條特定的邊走99步,可不一定只能到一個點啊,哈哈!

  這就是問題所在,所以我們調整生長點為(root,’a’,2)的時候,如果從當前活躍點開始,沿著活躍邊走了活躍半徑個距離,我們停在活躍邊上面一個隱式節點那就沒事,要是停到了內部節點node1,那我們就要更新活躍點為這個內部節點啦!然後調整三元組為(node1,’\0’,0)了。這裡假設我們將(root,’a’,2)更新為(node1,’\0’,0)。我們再來看一下,假設下一個字元是c,且node1後面有一個邊是以c為首字母的,於是更新三元組為(node1,’c’,1);假設我們之後要插入字元x,而從node1開始沒有那個邊是以x為首字母的,這時候就要插入葉子x了,代表了字尾abcx,我們下一個就要插入字尾bx了,但是怎麼定位到下一個生長點?跟前面一樣更新生長點為(node1,’b’,0)?

  肯定不行啊,因為下一個生長點根本就不在node1的子樹裡面。為什麼?因為node1的子樹裡面任何一個點代表的字串都是以ab(也就是根節點到node1那個邊),而我們插入的是bcx。那麼下一個生長點是什麼呢?我們猜測一下啊,應該是(node2,’c’,1)其中根節點到node2的路徑上面的字元是b!這樣就可以啦。首先我們來明確一下存不存在這樣一個結果node2的生長點?是存在的,因為既然後綴樹中有ab這個內部節點(注意我的用詞,ab已經成為i類不節點了,而不是剛剛建立的),那麼必然有b這個內部節點(因為假設node1後面的兩個兒子分別為x,y,那麼字尾樹中存在abx,和aby那麼也必然存在bx,by所以也必然存在node2);同時字尾樹中有字串abc,那麼必然也有串bc,於是證明是有的!下一個問題是,我們怎麼快速將生長點從(node1,’c’,1)定位到(node2,’c’,1),所以我們想要是有一條指標從node1指向node2就好了,所以我們應該之前建立這個字尾連結串列這樣下次就方便了。

  什麼時候建立呢?我們來看啊,當初我們建立ab這個內部節點的時候,內部節點或者是已經存在了或者下一步就存在(注意跟上面的對比啊,這次是說我們當初建立ab內部節點的時候,所以這個內部節點是剛建立的,故而內部節點b暫時還不一定存在)為什麼說下一步就存在呢?(因為: 如果 aS 停在一個內部節點上面,也就是 aS 後有分支,那麼當前的 T[0 .. i-1] 肯定有子串 aS+c 以及aS+d ( c 和 d 是不同的兩個字元) 這兩個不同的子串,於是肯定也有 S+c 以及 S+d 兩個子串了。至於“下次擴充套件時產生”的這種情況,則發生在已經有 aS+c 、 S+c ,剛加入 aS+d (於是產生了新的內部節點),正要加入 S+d 的時候。)下面我們來看啊,既然”下一步”內部節點b一定存在了,那就在”下一步”的時候建立指標從內部節點ab指向內部節點b。

  當我們走到字尾連結串列的尾部時候,意味著我們已經處理完畢remainder的前兩個(就是從根節點到node1的邊長,這兩個字尾意味著abcx,bcx)”欠債”。於是我們回到根節點,當然了,活躍邊和活躍半徑不變。那麼為什麼此時從根節點出發沿著活躍邊走活躍半徑個字元就能唯一鎖定生長點呢?

  答案是這樣的,因為啊,我們之前將活躍點更新為node1之後可能還是會更新活躍點,我們將最後更新的活躍點命名為nodeN,此時的三元組為(nodeN,active_edge,active_length),我們知道這個三元組確定的生長點位於nodeN的某條邊上的一個隱式節點。我們知道我們路過nodeN這個活躍點的時候,nodeN那邊已經存在後綴連結了,所以當我們沿著字尾連結插入葉子,在最終回到根節點時候三元組變為nodeN(root,active_edge,active_length)。一步定位!而且在沿著字尾連結串列定位生長點的時候也是一步定位!

 

     下面說明一下為什麼上述演算法是線性的。首先啊,我們來看上面一共涉及到了幾個操作:插入葉節點(每次插入消耗常數時間,總葉節點|T|個,故而總時間為O(|T|),建立字尾連結串列(每次建立一個指標只需要沿著前面建好的字尾連結串列走一下,然後一步定位,所以每次建立一個指標只需要常數時間;而每次我們沿著字尾連結串列走都會插入新葉子,故而後綴連結串列一共也只有|T|這麼長,故而一共需要插入O(|T|)次),更新三元組(因為涉及到活躍點的移動,故而我們這麼看,沒有更新活躍點的時候是常數時間,而更新活躍點的情況有點複雜,我們這麼理解:每次更新活躍點都會導致活躍半徑相應減小,而活躍半徑始在整個建樹的過程中最多增加|T|,故而整個更新活躍點所需時間為O(|T|)),其他諸如remainder的調整等等,每次花費常數時間,總和為O(|T|)。綜上所述,線性演算法降臨了!!!!

 

程式碼實現(參考版):

C#:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace SuffixTreeAlgorithm
{
    public class SuffixTree
    {
        public char? CanonizationChar { get; set; }
        public string Word { get; private set; }
        private int CurrentSuffixStartIndex { get; set; }
        private int CurrentSuffixEndIndex { get; set; }
        private Node LastCreatedNodeInCurrentIteration { get; set; }
        private int UnresolvedSuffixes { get; set; }
        public Node RootNode { get; private set; }
        private Node ActiveNode { get; set; }
        private Edge ActiveEdge { get; set; }
        private int DistanceIntoActiveEdge { get; set; }
        private char LastCharacterOfCurrentSuffix { get; set; }
        private int NextNodeNumber { get; set; }
        private int NextEdgeNumber { get; set; }

        public SuffixTree(string word)
        {
            Word = word;
            RootNode = new Node(this);
            ActiveNode = RootNode;
        }

        public event Action<SuffixTree> Changed;
        private void TriggerChanged()
        {
            var handler = Changed;
            if(handler != null)
                handler(this);
        }

        public event Action<string, object[]> Message;
        private void SendMessage(string format, params object[] args)
        {
            var handler = Message;
            if(handler != null)
                handler(format, args);
        }

        public static SuffixTree Create(string word, char canonizationChar = '$')
        {
            var tree = new SuffixTree(word);
            tree.Build(canonizationChar);
            return tree;
        }

        public void Build(char canonizationChar)
        {
            var n = Word.IndexOf(Word[Word.Length - 1]);
            var mustCanonize = n < Word.Length - 1;
            if(mustCanonize)
            {
                CanonizationChar = canonizationChar;
                Word = string.Concat(Word, canonizationChar);
            }

            for(CurrentSuffixEndIndex = 0; CurrentSuffixEndIndex < Word.Length; CurrentSuffixEndIndex++)
            {
                SendMessage("=== ITERATION {0} ===", CurrentSuffixEndIndex);
                LastCreatedNodeInCurrentIteration = null;
                LastCharacterOfCurrentSuffix = Word[CurrentSuffixEndIndex];

                for(CurrentSuffixStartIndex = CurrentSuffixEndIndex - UnresolvedSuffixes; CurrentSuffixStartIndex <= CurrentSuffixEndIndex; CurrentSuffixStartIndex++)
                {
                    var wasImplicitlyAdded = !AddNextSuffix();
                    if(wasImplicitlyAdded)
                    {
                        UnresolvedSuffixes++;
                        break;
                    }
                    if(UnresolvedSuffixes > 0)
                        UnresolvedSuffixes--;
                }
            }
        }

        private bool AddNextSuffix()
        {
            var suffix = string.Concat(Word.Substring(CurrentSuffixStartIndex, CurrentSuffixEndIndex - CurrentSuffixStartIndex), "{", Word[CurrentSuffixEndIndex], "}");
            SendMessage("The next suffix of '{0}' to add is '{1}' at indices {2},{3}", Word, suffix, CurrentSuffixStartIndex, CurrentSuffixEndIndex);
            SendMessage(" => ActiveNode:             {0}", ActiveNode);
            SendMessage(" => ActiveEdge:             {0}", ActiveEdge == null ? "none" : ActiveEdge.ToString());
            SendMessage(" => DistanceIntoActiveEdge: {0}", DistanceIntoActiveEdge);
            SendMessage(" => UnresolvedSuffixes:     {0}", UnresolvedSuffixes);
            if(ActiveEdge != null && DistanceIntoActiveEdge >= ActiveEdge.Length)
                throw new Exception("BOUNDARY EXCEEDED");

            if(ActiveEdge != null)
                return AddCurrentSuffixToActiveEdge();

            if(GetExistingEdgeAndSetAsActive())
                return false;

            ActiveNode.AddNewEdge();
            TriggerChanged();

            UpdateActivePointAfterAddingNewEdge();
            return true;
        }

        private bool GetExistingEdgeAndSetAsActive()
        {
            Edge edge;
            if(ActiveNode.Edges.TryGetValue(LastCharacterOfCurrentSuffix, out edge))
            {
                SendMessage("Existing edge for {0} starting with '{1}' found. Values adjusted to:", ActiveNode, LastCharacterOfCurrentSuffix);
                ActiveEdge = edge;
                DistanceIntoActiveEdge = 1;
                TriggerChanged();

                NormalizeActivePointIfNowAtOrBeyondEdgeBoundary(ActiveEdge.StartIndex);
                SendMessage(" => ActiveEdge is now: {0}", ActiveEdge);
                SendMessage(" => DistanceIntoActiveEdge is now: {0}", DistanceIntoActiveEdge);
                SendMessage(" => UnresolvedSuffixes is now: {0}", UnresolvedSuffixes);

                return true;
            }
            SendMessage("Existing edge for {0} starting with '{1}' not found", ActiveNode, LastCharacterOfCurrentSuffix);
            return false;
        }

        private bool AddCurrentSuffixToActiveEdge()
        {
            var nextCharacterOnEdge = Word[ActiveEdge.StartIndex + DistanceIntoActiveEdge];
            if(nextCharacterOnEdge == LastCharacterOfCurrentSuffix)
            {
                SendMessage("The next character on the current edge is '{0}' (suffix added implicitly)", LastCharacterOfCurrentSuffix);
                DistanceIntoActiveEdge++;
                TriggerChanged();

                SendMessage(" => DistanceIntoActiveEdge is now: {0}", DistanceIntoActiveEdge);
                NormalizeActivePointIfNowAtOrBeyondEdgeBoundary(ActiveEdge.StartIndex);

                return false;
            }

            SplitActiveEdge();
            ActiveEdge.Tail.AddNewEdge();
            TriggerChanged();

            UpdateActivePointAfterAddingNewEdge();

            return true;
        }

        private void UpdateActivePointAfterAddingNewEdge()
        {
            if(ReferenceEquals(ActiveNode, RootNode))
            {
                if(DistanceIntoActiveEdge > 0)
                {
                    SendMessage("New edge has been added and the active node is root. The active edge will now be updated.");
                    DistanceIntoActiveEdge--;
                    SendMessage(" => DistanceIntoActiveEdge decremented to: {0}", DistanceIntoActiveEdge);
                    ActiveEdge = DistanceIntoActiveEdge == 0 ? null : ActiveNode.Edges[Word[CurrentSuffixStartIndex + 1]];
                    SendMessage(" => ActiveEdge is now: {0}", ActiveEdge);
                    TriggerChanged();

                    NormalizeActivePointIfNowAtOrBeyondEdgeBoundary(CurrentSuffixStartIndex + 1);
                }
            }
            else
                UpdateActivePointToLinkedNodeOrRoot();
        }

        private void NormalizeActivePointIfNowAtOrBeyondEdgeBoundary(int firstIndexOfOriginalActiveEdge)
        {
            var walkDistance = 0;
            while(ActiveEdge != null && DistanceIntoActiveEdge >= ActiveEdge.Length)
            {
                SendMessage("Active point is at or beyond edge boundary and will be moved until it falls inside an edge boundary");
                DistanceIntoActiveEdge -= ActiveEdge.Length;
                ActiveNode = ActiveEdge.Tail ?? RootNode;
                if(DistanceIntoActiveEdge == 0)
                    ActiveEdge = null;
                else
                {
                    walkDistance += ActiveEdge.Length;
                    var c = Word[firstIndexOfOriginalActiveEdge + walkDistance];
                    ActiveEdge = ActiveNode.Edges[c];
                }
                TriggerChanged();
            }
        }

        private void SplitActiveEdge()
        {
            ActiveEdge = ActiveEdge.SplitAtIndex(ActiveEdge.StartIndex + DistanceIntoActiveEdge);
            SendMessage(" => ActiveEdge is now: {0}", ActiveEdge);
            TriggerChanged();
            if(LastCreatedNodeInCurrentIteration != null)
            {
                LastCreatedNodeInCurrentIteration.LinkedNode = ActiveEdge.Tail;
                SendMessage(" => Connected {0} to {1}", LastCreatedNodeInCurrentIteration, ActiveEdge.Tail);
                TriggerChanged();
            }
            LastCreatedNodeInCurrentIteration = ActiveEdge.Tail;
        }

        private void UpdateActivePointToLinkedNodeOrRoot()
        {
            SendMessage("The linked node for active node {0} is {1}", ActiveNode, ActiveNode.LinkedNode == null ? "[null]" : ActiveNode.LinkedNode.ToString());
            if(ActiveNode.LinkedNode != null)
            {
                ActiveNode = ActiveNode.LinkedNode;
                SendMessage(" => ActiveNode is now: {0}", ActiveNode);
            }
            else
            {
                ActiveNode = RootNode;
                SendMessage(" => ActiveNode is now ROOT", ActiveNode);
            }
            TriggerChanged();

            if(ActiveEdge != null)
            {
                var firstIndexOfOriginalActiveEdge = ActiveEdge.StartIndex;
                ActiveEdge = ActiveNode.Edges[Word[ActiveEdge.StartIndex]];
                TriggerChanged();
                NormalizeActivePointIfNowAtOrBeyondEdgeBoundary(firstIndexOfOriginalActiveEdge);
            }
        }

        public string RenderTree()
        {
            var writer = new StringWriter();
            RootNode.RenderTree(writer, "");
            return writer.ToString();
        }

        public string WriteDotGraph()
        {
            var sb = new StringBuilder();
            sb.AppendLine("digraph {");
            sb.AppendLine("rankdir = LR;");
            sb.AppendLine("edge [arrowsize=0.5,fontsize=11];");
            for(var i = 0; i < NextNodeNumber; i++)
                sb.AppendFormat("node{0} [label=\"{0}\",style=filled,fillcolor={1},shape=circle,width=.1,height=.1,fontsize=11,margin=0.01];",
                    i, ActiveNode.NodeNumber == i ? "cyan" : "lightgrey").AppendLine();
            RootNode.WriteDotGraph(sb);
            sb.AppendLine("}");
            return sb.ToString();
        }

        public HashSet<string> ExtractAllSubstrings()
        {
            var set = new HashSet<string>();
            ExtractAllSubstrings("", set, RootNode);
            return set;
        }

        private void ExtractAllSubstrings(string str, HashSet<string> set, Node node)
        {
            foreach(var edge in node.Edges.Values)
            {
                var edgeStr = edge.StringWithoutCanonizationChar;
                var edgeLength = !edge.EndIndex.HasValue && CanonizationChar.HasValue ? edge.Length - 1 : edge.Length; // assume tailing canonization char
                for(var length = 1; length <= edgeLength; length++)
                    set.Add(string.Concat(str, edgeStr.Substring(0, length)));
                if(edge.Tail != null)
                    ExtractAllSubstrings(string.Concat(str, edge.StringWithoutCanonizationChar), set, edge.Tail);
            }
        }

        public List<string> ExtractSubstringsForIndexing(int? maxLength = null)
        {
            var list = new List<string>();
            ExtractSubstringsForIndexing("", list, maxLength ?? Word.Length, RootNode);
            return list;
        }

        private void ExtractSubstringsForIndexing(string str, List<string> list, int len, Node node)
        {
            foreach(var edge in node.Edges.Values)
            {
                var newstr = string.Concat(str, Word.Substring(edge.StartIndex, Math.Min(len, edge.Length)));
                if(len > edge.Length && edge.Tail != null)
                    ExtractSubstringsForIndexing(newstr, list, len - edge.Length, edge.Tail);
                else
                    list.Add(newstr);
            }
        }

        public class Edge
        {
            private readonly SuffixTree _tree;

            public Edge(SuffixTree tree, Node head)
            {
                _tree = tree;
                Head = head;
                StartIndex = tree.CurrentSuffixEndIndex;
                EdgeNumber = _tree.NextEdgeNumber++;
            }

            public Node Head { get; private set; }
            public Node Tail { get; private set; }
            public int StartIndex { get; private set; }
            public int? EndIndex { get; set; }
            public int EdgeNumber { get; private set; }
            public int Length { get { return (EndIndex ?? _tree.Word.Length - 1) - StartIndex + 1; } }

            public Edge SplitAtIndex(int index)
            {
                _tree.SendMessage("Splitting edge {0} at index {1} ('{2}')", this, index, _tree.Word[index]);
                var newEdge = new Edge(_tree, Head);
                var newNode = new Node(_tree);
                newEdge.Tail = newNode;
                newEdge.StartIndex = StartIndex;
                newEdge.EndIndex = index - 1;
                Head = newNode;
                StartIndex = index;
                newNode.Edges.Add(_tree.Word[StartIndex], this);
                newEdge.Head.Edges[_tree.Word[newEdge.StartIndex]] = newEdge;
                _tree.SendMessage(" => Hierarchy is now: {0} --> {1} --> {2} --> {3}", newEdge.Head, newEdge, newNode, this);
                return newEdge;
            }

            public override string ToString()
            {
                return string.Concat(_tree.Word.Substring(StartIndex, (EndIndex ?? _tree.CurrentSuffixEndIndex) - StartIndex + 1), "(",
                    StartIndex, ",", EndIndex.HasValue ? EndIndex.ToString() : "#", ")");
            }

            public string StringWithoutCanonizationChar
            {
                get { return _tree.Word.Substring(StartIndex, (EndIndex ?? _tree.CurrentSuffixEndIndex - (_tree.CanonizationChar.HasValue ? 1 : 0)) - StartIndex + 1); }
            }

            public string String
            {
                get { return _tree.Word.Substring(StartIndex, (EndIndex ?? _tree.CurrentSuffixEndIndex) - StartIndex + 1); }
            }

            public void RenderTree(TextWriter writer, string prefix, int maxEdgeLength)
            {
                var strEdge = _tree.Word.Substring(StartIndex, (EndIndex ?? _tree.CurrentSuffixEndIndex) - StartIndex + 1);
                writer.Write(strEdge);
                if(Tail == null)
                    writer.WriteLine();
                else
                {
                    var line = new string(RenderChars.HorizontalLine, maxEdgeLength - strEdge.Length + 1);
                    writer.Write(line);
                    Tail.RenderTree(writer, string.Concat(prefix, new string(' ', strEdge.Length + line.Length)));
                }
            }

            public void WriteDotGraph(StringBuilder sb)
            {
                if(Tail == null)
                    sb.AppendFormat("leaf{0} [label=\"\",shape=point]", EdgeNumber).AppendLine();
                string label, weight, color;
                if(_tree.ActiveEdge != null && ReferenceEquals(this, _tree.ActiveEdge))
                {
                    if(_tree.ActiveEdge.Length == 0)
                        label = "";
                    else if(_tree.DistanceIntoActiveEdge > Length)
                        label = "<<FONT COLOR=\"red\" SIZE=\"11\"><B>" + String + "</B> (" + _tree.DistanceIntoActiveEdge + ")</FONT>>";
                    else if(_tree.DistanceIntoActiveEdge == Length)
                        label = "<<FONT COLOR=\"red\" SIZE=\"11\">" + String + "</FONT>>";
                    else if(_tree.DistanceIntoActiveEdge > 0)
                        label = "<<TABLE BORDER=\"0\" CELLPADDING=\"0\" CELLSPACING=\"0\"><TR><TD><FONT COLOR=\"blue\"><B>" + String.Substring(0, _tree.DistanceIntoActiveEdge) + "</B></FONT></TD><TD COLOR=\"black\">" + String.Substring(_tree.DistanceIntoActiveEdge) + "</TD></TR></TABLE>>";
                    else
                        label = "\"" + String + "\"";
                    color = "blue";
                    weight = "5";
                }
                else
                {
                    label = "\"" + String + "\"";
                    color = "black";
                    weight = "3";
                }
                var tail = Tail == null ? "leaf" + EdgeNumber : "node" + Tail.NodeNumber;
                sb.AppendFormat("node{0} -> {1} [label={2},weight={3},color={4},size=11]", Head.NodeNumber, tail, label, weight, color).AppendLine();
                if(Tail != null)
                    Tail.WriteDotGraph(sb);
            }
        }

        public class Node
        {
            private readonly SuffixTree _tree;

            public Node(SuffixTree tree)
            {
                _tree = tree;
                Edges = new Dictionary<char, Edge>();
                NodeNumber = _tree.NextNodeNumber++;
            }

            public Dictionary<char, Edge> Edges { get; private set; }
            public Node LinkedNode { get; set; }
            public int NodeNumber { get; private set; }

            public void AddNewEdge()
            {
                _tree.SendMessage("Adding new edge to {0}", this);
                var edge = new Edge(_tree, this);
                Edges.Add(_tree.Word[_tree.CurrentSuffixEndIndex], edge);
                _tree.SendMessage(" => {0} --> {1}", this, edge);
            }

            public void RenderTree(TextWriter writer, string prefix)
            {
                var strNode = string.Concat("(", NodeNumber.ToString(new string('0', _tree.NextNodeNumber.ToString().Length)), ")");
                writer.Write(strNode);
                var edges = Edges.Select(kvp => kvp.Value).OrderBy(e => _tree.Word[e.StartIndex]).ToArray();
                if(edges.Any())
                {
                    var prefixWithNodePadding = prefix + new string(' ', strNode.Length);
                    var maxEdgeLength = edges.Max(e => (e.EndIndex ?? _tree.CurrentSuffixEndIndex) - e.StartIndex + 1);
                    for(var i = 0; i < edges.Length; i++)
                    {
                        char connector, extender = ' ';
                        if(i == 0)
                        {
                            if(edges.Length > 1)
                            {
                                connector = RenderChars.TJunctionDown;
                                extender = RenderChars.VerticalLine;
                            }
                            else
                                connector = RenderChars.HorizontalLine;
                        }
                        else
                        {