Ukkonen's suffix tree algorithm in plain English
在这一点上我觉得有点发胖。我花了好几天的时间试图把我的头完全围绕在后缀树的构造上,但是因为我没有数学背景,很多解释在我开始过度使用数学符号的时候就消失了。我发现最接近一个很好的解释是用后缀树快速搜索字符串,但是他掩盖了不同的点,算法的某些方面仍然不清楚。
我敢肯定,对于除我之外的许多其他人来说,在堆栈溢出中逐步解释这个算法是非常宝贵的。
作为参考,这里是Ukkonen关于算法的论文:http://www.cs.helsinki.fi/u/ukkonen/suffixt1withfigs.pdf
到目前为止,我的基本理解是:
- 我需要迭代给定字符串t的每个前缀p
- 我需要遍历前缀p中的每个后缀s并将其添加到树中
- 要将后缀s添加到树中,我需要迭代s中的每个字符,迭代过程包括沿着以s中相同的字符集c开头的现有分支遍历,并在后缀中到达不同的字符时可能将边缘拆分为子代节点,或者如果没有匹配的边缘要遍历,则执行以下操作WN。当找不到匹配的边来遍历C时,将为C创建一个新的叶边。
正如大多数解释中指出的,基本算法似乎是O(n2),因为我们需要单步执行所有前缀,然后我们需要单步执行每个前缀的每个后缀。Ukkonen的算法显然是独一无二的,因为他使用的后缀指针技术,尽管我认为这是我难以理解的。
我也很难理解:
- 准确分配、使用和更改"活动点"的时间和方式
- 算法的规范化方面发生了什么?
- 为什么我看到的实现需要"修复"它们正在使用的边界变量
这是完整的C源代码。它不仅工作正常,而且支持自动规范化,并呈现出更好的输出文本图形。源代码和示例输出位于:
https://gist.github.com/2373868
更新日期:2017-11-04
多年后,我发现了后缀树的新用途,并在javascript中实现了该算法。要点如下。它应该是无缺陷的。将它从同一位置转储到JS文件
https://gist.github.com/axefrog/c347bf0f5e0723cbd09b1aaed6ec6fc6
下面尝试描述Ukkonen算法,首先显示字符串简单时的操作(即不包含任何重复字符),然后将其扩展到完整算法。好的。
首先,一些初步声明。好的。
我们正在建造的,基本上就像一个搜索引擎。所以那里是一个根节点,边缘会从中流出,导致新节点,以及从这些边缘向外延伸,等等好的。
但是:与搜索trie不同,边缘标签不是单一的字符。相反,每个边都用一对整数标记
基本原理
我想首先演示如何创建特别简单的字符串,没有重复字符的字符串:好的。
1 | abc |
该算法从左到右分步骤工作。字符串中的每个字符都有一个步骤。每个步骤可能涉及多个单独的操作,但我们将看到(最后的观察结果)操作总数是O(N)。好的。
所以,我们从左边开始,首先只插入一个字符
所以我们有一个初始树,它看起来像这样:好的。
好的。
意思是:好的。
1 | abcabxabcd |
它从
步骤1到3:在前3个步骤之后,我们得到上一个示例中的树:好的。
和往常一样,
现在在步骤
尝试在活动点插入
我尝试使用jojojapan的答案中给出的方法实现后缀树,但由于规则中使用的措辞,它在某些情况下不起作用。此外,我还提到,没有人能够使用这种方法实现绝对正确的后缀树。下面我将写一个"概述"Jojojapan的答案,并对规则做一些修改。我还将描述当我们忘记创建重要的后缀链接时的情况。
使用的附加变量
让我们使用一个内部节点的概念——除了根节点和叶节点之外,所有的节点都是内部节点。
观察1
当我们需要插入的最后一个后缀已经存在于树中时,树本身就不会发生任何变化(我们只更新
观察2
如果在某一点上,
现在,让我们重新定义规则:
规则1
If after an insertion from the active node = root, the active length is greater than 0, then:
active node is not changed active length is decremented active edge is shifted right (to the first character of the next suffix we must insert)
规则2
If we create a new internal node OR make an inserter from an internal node, and this is not the first SUCH internal node at current step, then we link the previous SUCH node with THIS one through a suffix link.
规则3
After an insert from the active node which is not the root node, we must follow the suffix link and set the active node to the node it points to. If there is no a suffix link, set the active node to the root node. Either way, active edge and active length stay unchanged.
在这个
最后,观察3:
当我们要添加到树的符号已经在边缘时,我们根据
如果我们在这种情况下添加了后缀链接,那么让我们看看cdddcdc的后缀树示例,如果我们没有:
如果我们不通过后缀链接连接节点:
- 在添加最后一个字母c之前:
active node被设置为红色节点。但我们不从红色节点进行插入,因为字母"c"已经在边缘。这是否意味着蓝色节点必须没有后缀链接?不,我们必须通过后缀链接将蓝色节点连接到红色节点。为什么是正确的?因为Active Point方法保证我们到达正确的位置,即到下一个必须处理较短后缀插入的位置。
最后,下面是后缀树的实现:
希望这个"概述"与Jojojapan的详细答案相结合,将有助于某人实现自己的后缀树。
感谢@jojojapan的讲解,我在python中实现了这个算法。
@jojojojapan提到的几个小问题比我预期的要复杂,需要非常小心地处理。我花了几天时间使我的实现足够健壮(我想)。问题及解决办法如下:
以
跳过节点:当我们跟踪后缀链接时,更新活动点,然后发现其活动长度组件与新的活动节点不匹配。我们必须前进到正确的地方去分裂,或者插入一片叶子。这个过程可能不那么简单,因为在移动actlength和actedge的过程中一直在变化,当您必须移回根节点时,actedge和actlength可能因为这些移动而出错。我们需要额外的变量来保存这些信息。
@managonov不知何故指出了另外两个问题
分割可能会在尝试分割边缘时退化,有时您会发现分割操作正好在节点上。在这种情况下,我们只需要向该节点添加一个新的叶,将其作为标准的边缘分割操作,这意味着如果有后缀链接,就应该相应地维护它。
隐藏的后缀链接还有另一个特殊情况,这是由问题1和问题2引起的。有时我们需要跳几个节点到正确的点进行拆分,如果我们通过比较剩余的字符串和路径标签来移动,可能会超过正确的点。在这种情况下,后缀链接将被无意中忽略(如果应该有)。这可以通过向前移动时记住正确的点来避免。如果拆分节点已经存在,或者在展开步骤中出现问题1,则应保持后缀链接。
最后,我在python中的实现如下:
- Python
Tips: It includes a naive tree printing function in the code above, which is very important while debugging. It saved me a lot of
time and is convenient for locating special cases.
我的直觉是:
在主循环的k次迭代之后,您构建了一个后缀树,其中包含以前k个字符开始的完整字符串的所有后缀。
开始时,这意味着后缀树包含一个表示整个字符串的根节点(这是唯一从0开始的后缀)。
在len(string)迭代之后,您有一个包含所有后缀的后缀树。
在循环过程中,关键是活动点。我猜这代表了后缀树中最深的点,它对应于字符串前k个字符的适当后缀。(我认为正确的意思是后缀不能是整个字符串。)
例如,假设您看到了字符"abcabc"。活动点将表示树中与后缀"abc"对应的点。
活动点由(原点、第一个、最后一个)表示。这意味着您当前处于树中的某个点上,可以从节点原点开始,然后以字符串形式输入字符[第一个:最后一个]
添加新字符时,将查看活动点是否仍在现有树中。如果是这样,你就完了。否则,您需要在活动点向后缀树添加一个新节点,回滚到下一个最短匹配,然后再次检查。
注1:后缀指针为每个节点提供到下一个最短匹配的链接。
注2:添加新节点并回退时,将为新节点添加新的后缀指针。此后缀指针的目标将是位于缩短的活动点的节点。此节点将已经存在,或者将在此回退循环的下一次迭代中创建。
注3:规范化部分简单地节省了检查活动点的时间。例如,假设您总是使用origin=0,并且只更改了第一个和最后一个。要检查活动点,您必须每次沿着所有中间节点跟踪后缀树。通过只记录到最后一个节点的距离来缓存跟踪此路径的结果是有意义的。
你能给一个你所说的"固定"边界变量的代码例子吗?
健康警告:我也发现这个算法特别难理解,所以请意识到这个直觉很可能在所有重要细节上都是错误的…
@Jogojapan你带来了很棒的解释和可视化。但是正如@makagonov提到的,它缺少一些设置后缀链接的规则。在http://brenden.github.io/ukkonen-animation/通过单词"aabaab"一步一步地进行时,可以很好地看到它。当您从步骤10转到步骤11时,从节点5到节点2没有后缀链接,但是活动点突然移动到那里。
Makangov,因为我生活在爪哇世界,我也尝试遵循您的实现,以掌握ST建设工作流程,但这对我来说很难,因为:
- 将边与节点组合
- 使用索引指针而不是引用
- 中断语句;
- 继续陈述;
因此,我最终在爪哇实现了这样的实现,我希望以更清晰的方式反映所有的步骤,并减少其他Java用户的学习时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | import java.util.arrays;导入java.util.hashmap;导入java.util.map;公共类ST{公共类节点{私人最终利息ID;私有最终映射<character,edge>edges;私有节点slink;公共节点(最终int id){这个ID=ID;this.edges=new hashmap<>();}公共void setslink(最终节点slink){this.slink=slink;}公共映射<character,edge>getedges()。{返回this.edges;}公共节点getslink()。{返回this.slink;}公共字符串ToString(最终字符串字){返回新的StringBuilder()。追加("{")追加("ID")追加(":").append(此.id)追加(",").append("slink")。追加(":").append(this.slink!= NULL?this.slink.id:空)追加(",").append("边")。追加(":").append(edgestostring(word))。追加()<hr><P>嗨,我已经尝试在Ruby中实现上面解释的实现,请检查一下。它似乎工作得很好。</P><P>实现中唯一的区别是,我尝试使用边缘对象而不是仅仅使用符号。</P><P>它也出现在https://gist.github.com/suchitpuri/9304856</P>[cc] require 'pry' class Edge attr_accessor :data , :edges , :suffix_link def initialize data @data = data @edges = [] @suffix_link = nil end def find_edge element self.edges.each do |edge| return edge if edge.data.start_with? element end return nil end end class SuffixTrees attr_accessor :root , :active_point , :remainder , :pending_prefixes , :last_split_edge , :remainder def initialize @root = Edge.new nil @active_point = { active_node: @root , active_edge: nil , active_length: 0} @remainder = 0 @pending_prefixes = [] @last_split_edge = nil @remainder = 1 end def build string string.split("").each_with_index do |element , index| add_to_edges @root , element update_pending_prefix element add_pending_elements_to_tree element active_length = @active_point[:active_length] # if(@active_point[:active_edge] && @active_point[:active_edge].data && @active_point[:active_edge].data[0..active_length-1] == @active_point[:active_edge].data[active_length..@active_point[:active_edge].data.length-1]) # @active_point[:active_edge].data = @active_point[:active_edge].data[0..active_length-1] # @active_point[:active_edge].edges << Edge.new(@active_point[:active_edge].data) # end if(@active_point[:active_edge] && @active_point[:active_edge].data && @active_point[:active_edge].data.length == @active_point[:active_length] ) @active_point[:active_node] = @active_point[:active_edge] @active_point[:active_edge] = @active_point[:active_node].find_edge(element[0]) @active_point[:active_length] = 0 end end end def add_pending_elements_to_tree element to_be_deleted = [] update_active_length = false # binding.pry if( @active_point[:active_node].find_edge(element[0]) != nil) @active_point[:active_length] = @active_point[:active_length] + 1 @active_point[:active_edge] = @active_point[:active_node].find_edge(element[0]) if @active_point[:active_edge] == nil @remainder = @remainder + 1 return end @pending_prefixes.each_with_index do |pending_prefix , index| # binding.pry if @active_point[:active_edge] == nil and @active_point[:active_node].find_edge(element[0]) == nil @active_point[:active_node].edges << Edge.new(element) else @active_point[:active_edge] = node.find_edge(element[0]) if @active_point[:active_edge] == nil data = @active_point[:active_edge].data data = data.split("") location = @active_point[:active_length] # binding.pry if(data[0..location].join == pending_prefix or @active_point[:active_node].find_edge(element) != nil ) else #tree split split_edge data , index , element end end end end def update_pending_prefix element if @active_point[:active_edge] == nil @pending_prefixes = [element] return end @pending_prefixes = [] length = @active_point[:active_edge].data.length data = @active_point[:active_edge].data @remainder.times do |ctr| @pending_prefixes << data[-(ctr+1)..data.length-1] end @pending_prefixes.reverse! end def split_edge data , index , element location = @active_point[:active_length] old_edges = [] internal_node = (@active_point[:active_edge].edges != nil) if (internal_node) old_edges = @active_point[:active_edge].edges @active_point[:active_edge].edges = [] end @active_point[:active_edge].data = data[0..location-1].join @active_point[:active_edge].edges << Edge.new(data[location..data.size].join) if internal_node @active_point[:active_edge].edges << Edge.new(element) else @active_point[:active_edge].edges << Edge.new(data.last) end if internal_node @active_point[:active_edge].edges[0].edges = old_edges end #setup the suffix link if @last_split_edge != nil and @last_split_edge.data.end_with?@active_point[:active_edge].data @last_split_edge.suffix_link = @active_point[:active_edge] end @last_split_edge = @active_point[:active_edge] update_active_point index end def update_active_point index if(@active_point[:active_node] == @root) @active_point[:active_length] = @active_point[:active_length] - 1 @remainder = @remainder - 1 @active_point[:active_edge] = @active_point[:active_node].find_edge(@pending_prefixes.first[index+1]) else if @active_point[:active_node].suffix_link != nil @active_point[:active_node] = @active_point[:active_node].suffix_link else @active_point[:active_node] = @root end @active_point[:active_edge] = @active_point[:active_node].find_edge(@active_point[:active_edge].data[0]) @remainder = @remainder - 1 end end def add_to_edges root , element return if root == nil root.data = root.data + element if(root.data and root.edges.size == 0) root.edges.each do |edge| add_to_edges edge , element end end end suffix_tree = SuffixTrees.new suffix_tree.build("abcabxabcd") binding.pry |