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

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

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

本文介绍的是Ukkonen算法的第4部分,讲解了如何在$O(n)$时间内构建后缀树。

需要的数据结构

我们需要按照下面的方式进行修改:

  • 将每个新的内部节点作为所有之前插入过的后缀的分割点;
  • 将每个分割点存储在后缀链接中,以便找到其对应的较短的后缀;
  • 随着新的后缀的插入,将新的叶节点添加到树中。

为了实现这些修改,Ukkonen提出了特殊类型的节点,称为Active Point(活动点)和Remaining Suffix(剩余后缀)。

活动点

一个活动点由三个参数组成:

  1. active node(活动节点):代表当前节点;
  2. active edge(活动边):代表当前节点的一个子节点的边;
  3. active length(活动长度):代表从活动节点到当前位置的字符串长度。
剩余后缀

剩余后缀是指色来脚节点与当前位置之间的字符串。

构造后缀树

下面是构建后缀树的算法:

  1. 首先,将整个字符串添加到树中作为第一个后缀;
  2. 基于第1个后缀,初始化活动节点为根节点,并将第一个字符添加到树中;
  3. 从第2个字符开始,对于每个字符循环如下步骤:
    1. 按照剩余后缀规则移动活动点:
      • 如果节点上有前缀与待插入的字符匹配,则将活动长度+1。
      • 如果节点上没有前缀与待插入的字符匹配,则将活动点移动到下一个后缀链接。
      • 重复该过程,直到找到待插入字符的节点位置。
    2. 如果找不到待插入字符的节点位置,则将新的叶子节点添加到活动节点作为其子节点的边上。
    3. 如果找到了待插入字符的节点位置,则检查该位置上是否存在待插入字符的子节点。
      • 如果存在,则结束循环;
      • 如果不存在,则继续前往下一个字符,回到步骤3.1。

为了优化算法的速度,Ukkonen提出了常数时间内维护活动节点和剩余后缀的方法。这样,构建整个后缀树只需要$O(n)$的时间。

代码实现如下:

# 定义节点类
class Node:
    def __init__(self, start, end):
        self.children = {}
        self.start = start
        self.end = end
        self.link = None
        
    def add_child(self, key, value):
        self.children[key] = value
        
    def has_child(self, key):
        return key in self.children
    
    def get_child(self, key):
        return self.children[key]

# 构建后缀树
def build_suffix_tree(string):
    # 添加初始后缀
    string += "$"
    # 初始化根节点
    root = Node(-1, -1)
    # 初始化活动点
    active_node = root
    active_edge = ""
    active_length = 0
    # 初始化剩余后缀
    remainder = 0
    # 循环每个字符
    for i in range(len(string)):
        # 初始化当前字符
        current_char = string[i]
        # 将剩余后缀与当前字符进行匹配,并移动活动点
        last_node = None
        remainder += 1
        while remainder > 0:
            if active_length == 0:
                active_edge = current_char
            if not active_node.has_child(active_edge):
                # 如果当前节点没有该子节点,则创建一个新的叶子节点
                new_leaf = Node(i, len(string)-1)
                active_node.add_child(active_edge, new_leaf)
                # 创建后缀链接
                if last_node is not None:
                    last_node.link = active_node
                last_node = active_node
            else:
                # 获取该子节点
                next_node = active_node.get_child(active_edge)
                # 如果该子节点的前缀与当前字符匹配,则增加活动长度
                if match(next_node, string, i - remainder + 1, i):
                    active_length += 1
                    # 创建后缀链接
                    if last_node is not None:
                        last_node.link = active_node
                    last_node = active_node
                    # 结束循环
                    break
                else:
                    # 如果该子节点的前缀与当前字符不匹配,则将活动点移动到该子节点
                    split_end = next_node.start + active_length - 1
                    split_child = Node(next_node.start, split_end)
                    active_node.children[active_edge] = split_child
                    split_leaf = Node(i, len(string)-1)
                    split_child.add_child(current_char, split_leaf)
                    next_node.start += active_length
                    split_child.add_child(string[next_node.start], next_node)
                    # 创建后缀链接
                    if last_node is not None:
                        last_node.link = split_child
                    last_node = split_child
            remainder -= 1
            if active_node == root and active_length > 0:
                active_length -= 1
                active_edge = string[i-remainder+1]
            else:
                active_node = active_node.link if active_node.link is not None else root
        # 将当前字符作为下一个活动点的起始节点
        if active_node == root:
            active_edge = string[i+1]
        else:
            active_edge = string[active_node.start + active_length]
        # 初始化下一个活动点
        active_length = 0
        active_node = active_node if active_node != root else active_node.get_child(string[i+1])
        # 处理未分配的后缀链接
        if last_node is not None:
            last_node.link = active_node
    return root

# 辅助函数
def match(node, string, start, end):
    return (node.end - node.start) >= (end - start) and string[start:start + node.end - node.start + 1] == string[node.start:node.end + 1]

# 测试用例
root = build_suffix_tree("mississippi")
assert root.has_child("m")
assert root.has_child("i")
assert root.has_child("s")
assert not root.has_child("p")
assert root.get_child("i").has_child("s")
assert root.get_child("i").get_child("s").get_child("sippi").end == 9