📜  Ukkonen的后缀树构造–第1部分(1)

📅  最后修改于: 2023-12-03 15:05:42.695000             🧑  作者: Mango

Ukkonen的后缀树构造–第1部分

后缀树(Suffix Tree)是处理字符串算法中的一种基础数据结构,可以在 $O(n)$ 的时间复杂度内完成字符串的多种操作,例如查找子串、查找最长重复子串、查找最长回文等。Ukkonen的后缀树构造算法是目前时间复杂度最优的后缀树构造算法,其时间复杂度为 $O(n)$。这篇介绍将对Ukkonen算法进行详细讲解。

Ukkonen算法

Ukkonen算法是一个逐步构建后缀树的算法,不同于其他常规的后缀树构造算法,它是一个在线算法,无需预先知道整个字符串,而是动态添加字符,每添加一个字符就更新一次后缀树。

Ukkonen算法是由Esko Ukkonen在1995年发明的,他的论文中详细讲解了如何构建一棵后缀树,这里介绍Ukkonen算法的思路和实现。

1. 基本概念

在介绍Ukkonen算法之前,需要明确一些后缀树的基本概念:

  • 空洞节点:指的是没有用来表示任何字符串的节点,只是用来分隔边的节点。

  • 活动边(active edge):在构建过程中代表了正在扩展的边。

  • 活动点(active point):在构建过程中代表了当前正在处理的节点,由$active_edge$和$active_length$确定。

  • 等效类(equivalence class):后缀树是一棵多叉树,每个节点表示了一份(或多份)字符串。如果按照字符串的第一个字符将这些字符串分类,那么同一类别中的字符串将在树中形成一个子树,也就是等价类。

  • 后缀链接(suffix link):将等价类的根节点与它对应的后缀的起始位置的下一个字符所在的节点相连的链接,用于加速后续的查询。

2. 算法步骤

Ukkonen算法是一个基于边延伸的算法,其主要步骤如下:

  1. 初始化:空字符串 $s$ 可以看作是后缀树的根节点 .

  2. 依次插入字符串的所有字符,对于每一个字符 $c$, 扩展一条边:

    1. 通过$active_point$找到$active_edge$

    2. 对$active_edge$的标号所指向的子串进行检索以判断是否已经存在

    3. 找到在$active_edge$上第一个和$c$相同的字符,如果不存在则新建一条边

    4. 对于新扩展出来的边,如果其起点是一个空洞节点,就将其删除并将其终点移到$active_node$的位置

    5. 如果插入字符$c$后$active_node$不是整个字符串的后缀,则继续检索后继字符,直到最后一个字符结束

    6. 如果插入字符$c$后$active_node$是整个字符串的后缀,则不需要创建新的字符节点,直接更新$active_point$的$active_node$和$active_length$即可。

  3. 重复步骤 2 直到整个字符串处理完毕。

  4. 处理后缀链接。

整个算法的流程可以用下图表示:

Ukkonen_algorithm_flowchart

3. 代码实现

Ukkonen算法的实现较为复杂,主要涉及到多个方法,包括边扩展、后缀链接、可延续等状态的更新方法。Ukkonen算法的实现可以参考一些开源代码,比如C++实现

在此贴出主要的边扩展代码:

def extend(self, pos, symbol):
    """在后缀树上扩展边"""
    # 尝试重用上一次的结束点
    last = self.root
    self.remaining_suffix_count += 1
    while self.remaining_suffix_count > 0:
        if self.active_length == 0:
            self.active_edge = pos  # 设置开始位置
        if self.active_edge not in self.active_node.children:
            # 当前边不存在,新插入
            leaf = self.new_edge(pos, symbol)
            self.link(last, self.active_node, self.active_edge)
        else:
            next_node = self.active_node.children[self.active_edge]
            if self.exists_on_edge(next_node, pos):
                # 下一个字符已经存在,回收该字符
                self.active_edge = next_node.start + pos - next_node.end
                self.active_length += 1
                break
            split_edge = self.split_edge(next_node, pos)
            leaf = self.new_edge(pos, symbol)
            self.link(last, split_edge, symbol[split_edge.start])
            self.link(split_edge, leaf, symbol[pos])
        self.remaining_suffix_count -= 1
        if self.active_node == self.root and self.active_length > 0:
            self.active_length -= 1
            self.active_edge = pos - self.remaining_suffix_count + 1
        else:
            self.active_node = self.active_node.suffix_link
总结

Ukkonen算法是构建后缀树最为优秀的算法,该算法通过动态维护等效类,采用边延伸方式构建后缀树,避免了显式构建后缀子串的问题,提高了构建效率。该算法的具体实现也较为复杂,但是优秀的时间复杂度和性能使得其成为后缀树中最常用的构建算法之一。