我发现的所有有关Aho-Corasick的文献和实现都是关于预先用一组短语构建整个Trie的。但是,我对将其作为可变数据结构使用的方式感兴趣,该结构可以处理偶尔的添加和删除而无需重建整个Trie(假设其中有100万个条目)。只要最坏的情况很糟,只要平均情况接近对数,就可以。
根据我的看法,每个节点的失败状态是另一个使用相同符号的节点。因此,如果我们有一个从每个符号到使用该符号的节点列表的哈希多图,则我们的候选对象的失败状态需要更新。
删除非常简单。您会找到所有其他所有将已删除节点用作失败状态的节点,然后重新计算其失败状态。从字符串的末尾向后走,树应该仍处于良好状态。
添加有点棘手。未能通过该符号的任何节点都可以将新节点作为更好的候选者。但是,再次似乎足以遍历带有该符号的每个其他节点并完全重新计算其失败状态。
换句话说,如果我们要添加或删除带有符号“ A”的节点,则需要在特里树中的任意位置访问其他“ A”节点,并重新计算失败状态(寻找其最接近的祖先,带有“ A”(作为孩子或根)。这将需要访问符号“ A”的每个节点,但是对于我而言,这比访问特里树中的每个节点要小几个数量级。
该算法是否可行,或者我是否缺少明显的东西?
答案 0 :(得分:1)
我继续执行并实现了它,似乎正在起作用。对算法的更好描述,包括图片:https://burningmime.gitlab.io/setmatch/implementation-overview.html#supporting-add-remove-in-an-aho-corasick-trie
对于后代(以及根据StackOverflow策略),我将其复制到此处:
它支持修改(添加/删除)而不是全部构建 一次来自预设字典。任何文献都没有提到 关于它,以及我所见过的所有开源实现都有 在这方面紧随其后。事实证明,支持添加和删除 AC自动机的操作相当简单。深入尝试 小字母,不是很省时,但是幸运的是我们 在一个大字母上有一个浅的特里。
我们将每个令牌的哈希多图存储到使用该令牌的每个节点 令牌。删除词组时,我们从最后一个(最底部)开始 节点。我们从最底部的节点中删除了指向短语的指针。 然后,如果有任何子代,我们将无法再删除。不然就去 通过使用该节点作为失败状态的每个其他节点,以及 重新计算其失败状态。最后,删除节点,然后转到 节点的父节点并执行相同的过程。我们最终将达到 具有另一个词组输出的节点,子节点或根节点。
这很难想象,所以考虑一下这个尝试(被盗 摘自Wikipedia文章)。我们将删除字符串caa (浅灰色),这将要求失败状态显示为黄色 被更新:
结果:
请注意,在某些情况下,节点的失败状态可能会更新2或 在相同的删除操作中有更多次,但最终会 正确。可以通过先删除所有内容然后再删除来避免这种情况 回溯到令牌,但是代码很复杂,因为它 是的,那只会在某些方面提供性能优势 尴尬的情况,同时表现平均 案件。我们只是假设包含相同标记的字符串多于 曾经很少见。
添加节点稍微困难些,但是可以利用相同的节点 哈希多图结构。每次添加一个节点,其他每个节点 在使用相同令牌且深度比 添加的节点可能需要将其失败状态更新为新节点。
为说明这一点,请想象从第二张图回到 第一张图。相同的两个失败状态是 如果将字符串caa添加回该trie,则需要更新,并且 因为我们重新计算了每个c和一个节点,所以这不是问题。
我们可以跟踪节点深度以跳过大约一半,但是 现在,它只是重新计算每个节点的失败状态 共享令牌以节省内存。这意味着尝试 令牌出现很多次会很慢,但这又是 被认为是一种罕见的情况。如果您是 试图使这种技术适应DNA序列或 带有小字母的防病毒数据库。
时间复杂度取决于多少个节点共享符号以及如何 特里是最深的,这意味着它是最坏情况下的O(N),但实际上 相当小(平均情况大约为O(log ^ 2 N))。
令人难以置信的混乱且几乎没有注释的代码,但是如果您好奇的话,这里是the header,the body和some tests