广义的java_Ukkonen的广义后缀树算法

TL; DR

编辑(03/2019):我已经重新设计了我的实现,使用C 17 string_view 来表示我的子字符串以及一个确保引用字符串不会移动的缓存机制 . 更新版本可以在github上找到:https://github.com/Rerito/suffix-tree-v2 . 对于好奇的人来说,这是the github for my old implementation (in C++) . 哦,新的考虑还包括测试!

为了构建通用后缀树,不需要修改原始算法 .

详细分析

我得到的预感是正确的方式 . 为了跟上原始算法中使用的技巧,我们确实需要添加对原始字符串的引用 . 此外,该算法是在线的,这意味着您可以即时向树中添加字符串 .

假设我们对字符串(S1,...,SN)有一个广义后缀树GST(N) . 这里的问题是如何使用GST(N)处理GST(N 1)的构建过程 .

调整数据模型

在简单的情况下(单个后缀树),每个转换是一对(子串,末端顶点) . Ukkonen算法的技巧是使用一对指向原始字符串中适当位置的指针对子字符串进行建模 . 在这里,我们还需要将这样的子字符串链接到它的"parent"字符串 . 为此:

将原始字符串存储在哈希表中,为它们提供唯一的整数键 .

现在变为子串:(ID,(左指针,右指针)) . 因此我们可以使用ID来获取O(1)中的原始字符串 .

我们将其称为映射子字符串 . 我使用的C typedef是我原来的问题中找到的:

// This is a very basic draft of the Node class used

template

class Node {

typedef std::pair substring;

typedef std::pair mapped_substring;

typedef std::pair transition;

// C is the character type (basically `char`)

std::unordered_map g; // Called g just like in the article :)

Node *suffix_link;

};

正如您将看到的,我们也将保留参考对概念 . 这次,参考对就像转换一样,将保存一个映射的子串 .

注意:与C一样,字符串索引将从0开始 .

插入新字符串

我们想将SN 1插入GST(N) .

GST(N)可能已经有很多节点和转换 . 在一个简单的树中,我们只有root和特殊的sink节点 . 在这里,我们可能已经通过插入一些先前的字符串添加了SN 1的转换 . 首先要做的是,只要它与SN 1匹配,就可以沿树向下穿过过渡 .

通过这样做,我们以状态r结束 . 该状态可以是显式的(即,我们在顶点上正好结束)或隐式的(在转换的中间发生不匹配) . 我们使用与原始算法中相同的概念来模拟这样的状态:参考对 . 快速举例:

我们要插入SN 1 = banana

GST(N)中明确存在表示 ba 的节点

s对子字符串 nal 有一个转换t

当我们沿着树走下去时,我们最终会在角色 l 的过渡点t结束,这是一个不匹配 . 因此,我们得到的隐式状态r由参考对(s,m)表示,其中m是映射的子串(N 1,(1,3)) .

这里,r是构建 banana 的后缀树时算法的第5次迭代的活动点 . 该事实上,我们到达那个状态意味着 bana 的树已经建成了GST(N) .

在这个例子中,我们在第5次迭代中恢复算法,使用树为 bana 构建 banan 的后缀树 . 不要失去一般性,我们将声明r =(s,(N 1,(k,i-1)),i是第一个不匹配的指标 . 我们确实k≤i(这是一个同义词是r的同义词是一个明确的国家) .

Property: We can resume Ukkonen's algorithm to build GST(N) at iteration i (insertion of the character at index i in SN+1). The active point for this iteration is the state r we got by walking down the tree . 唯一需要调整的是一些解析子串的提取操作 .

该 properties 的证明

首先,这种状态r的存在意味着中间树T(N 1)i-1的整个状态也在那里 . 所以一切都已 Build ,我们恢复算法 .

我们需要证明算法中的每个过程都是有效的 . 有3个这样的子程序:

test_and_split :给定要在当前迭代中插入的字符,测试我们需要将转换拆分为两个单独的转换,如果当前点是终点 .

canonize :给定一个引用对(n,m),其中n是顶点,m是映射的子字符串,返回表示相同状态的对(n ', m'),例如m'是最短的子字符串 .

update :更新GST(N),使其在运行结束时具有中间树T(N 1)i的所有状态 .

test_and_split

Input: 顶点s,映射的子串m =(l,(k,p))和字符t .

Output: 一个布尔值,它告诉状态(s,m)是否是当前迭代的终点,而节点r是否显式表示(s,m),如果它不是终点 .

最简单的情况首先 . 如果子字符串为空(k> p),则状态已经明确表示 . 我们只需要测试我们是否达到了终点 . 在GST中,就像在公共后缀树中一样,每个节点最多有一个以给定字符开头的转换 ALWAYS . 因此,如果存在以t开始的转换,则返回true(我们到达终点),否则返回false .

现在是困难的部分,当k≤p . 我们首先需要获取原始字符串表中索引为l (*) 的字符串S1 .

令(l ', (k',p'))(相应的s')是与以字符S1(k) (*) 开始的s的转变TR相关的子串(相应的节点) . 存在这样的转变是因为(s,(l,(k,p))表示中间树T(N 1)i-1的边界路径上的(现有的)隐式状态 . 此外,我们是 sure ,即p - k此转换的第一个字符匹配 .

我们需要分裂这种转变吗?这取决于此过渡 ()** 上的Δ= p-k第1个字符 . 为了测试这个字符,我们需要获取位于散列表上索引l'的字符串,并获得索引为k'Δ的字符 . 这个字符保证存在,因为我们正在检查的状态是隐式的,因此在转换TR的中间结束(Δ≤p' - k') .

如果相等,我们无所事事并返回true(终点在这里)并且什么也不做 . 如果没有,那么我们必须拆分转换并创建一个新状态r . TR现在变为(l ', (k',k'Δ - 1))→r . 为r创建了另一个转换:(l', (k'Δ,p ') → s' . 我们现在返回false和r .

(*) :l不一定等于N 1.同样,l和l'可以不同(或相等) .

()** :请注意,数字Δ= p - k 1完全不依赖于选择作为映射子字符串的引用的字符串 . 它只取决于提供给例程的隐式状态 .

Canonize

Input: 节点_s_和表示树中现有状态e的映射子字符串(l,(k,p)) .

Output: 节点s'和映射的子字符串(l ',(k',p'))表示状态e的规范参考

使用相同的提取调整,我们只需要走下树直到我们已经用尽了字符"pool" . 在这里,就像 test_and_split 每个转换的唯一性以及我们将现有状态作为输入的事实为我们提供了有效的过程 .

更新

Input: 当前迭代的活动点和索引 .

Output: 下一次迭代的活动点 .

update 同时使用 canonize 和 test_and_split 这些都是GST友好的 . 后缀链接编辑与公共树的编辑完全相同 . 唯一需要注意的是,我们将使用SN 1作为参考字符串来创建开放转换(即通向节点的转换) . 因此,在迭代i,转换将始终链接到映射的子串(N 1,(i,∞))

最后一步

我们需要"close"开放过渡 . 为此,我们只是遍历它们并编辑∞远,用L-1替换它,其中L是SN 1的长度 . 我们还需要一个标记字符来标记字符串的结尾 . 我们肯定永远不会在任何字符串中遇到的角色 . 这样,叶子将永远留下叶子 .

结论

额外的读取工作增加了一些O(1)操作,增加了一点复杂性的常数因素 . 但是,渐近复杂度与插入的字符串的长度保持明显的线性关系 . 因此,从长度为n1,...,nN的字符串(S1,...,SN)构建GST(N)是:

c(GST(N))=Σi= 1..N ni

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值