📜  自动机派生树(1)

📅  最后修改于: 2023-12-03 14:57:07.339000             🧑  作者: Mango

自动机派生树

自动机派生树是基于字符串的自动机(例如AC自动机)的一种扩展,它能够记录每个字符串在自动机上的匹配路径和对应的出现次数。自动机派生树可以被用作字符串算法中多模式匹配、计算每个模式的出现次数等问题。

派生树结构

自动机派生树是一棵有根树,它的节点包含以下信息:

  • id:节点的唯一标识符
  • parent:节点的父节点
  • char:节点对应的字符
  • depth:节点深度
  • endpos:该字符串在匹配文本中的末位置集合
  • fail:在自动机上的失败指针
  • next:指向节点的子节点的哈希表
  • count:该字符串在匹配文本中的出现次数

每个节点都关联一条从根到本身的唯一路径,这个路径表示一个字符串在自动机上的匹配。对于每个节点,它的孩子节点就对应了所有以该字符串为前缀的出现位置。节点u的孩子v表示的是在文本串中有一个以节点u匹配的字符串形如c+W,其中c是某个字符,W是以节点v的字符串为前缀的子串。

构建过程

根据AC自动机的构建过程,我们可以得到一个包含了所有模式串在自动机上的匹配的Trie树,每个节点代表一种匹配。

接下来,可以按照以下过程构建自动机派生树:

  1. 将自动机上的所有节点(除根节点外)连成父子关系,同时记录每个节点的深度;
  2. 对于每个节点,将其对应的字符串的末位置加入到该节点的endpos集合中;
  3. 对于每个节点,将其父节点的fail指针指向该节点的所有字符串的最长后缀节点;
  4. 遍历所有节点,对于每个节点,更新该节点的子节点列表,将其加入到next哈希表中;
  5. 遍历所有节点,计算该节点的出现次数。

构建过程的时间复杂度为$O(N+MC)$,其中$N$是模式串总长度,$M$是模式串中相同字符的数量,$C$是字符集大小。这比起AC自动机的构建过程稍慢,但最终得到的数据结构能够支持更多的操作。

操作
多模式匹配

假设我们需要从一个文本串中找出所有在一个模式串集合中出现过的子串,可以按照以下步骤进行匹配:

  1. 从自动机根节点开始,对于每个字符$c$,沿着自动机的边移动,直到找到一个节点不存在对应的边或是遇到了一个字符串的末位置。
  2. 如果当前节点对应的字符串的末位置集合非空,那么该字符串一定匹配上了文本串的某个子串,记录该字符串出现的位置等信息。
  3. 沿着该节点的fail指针转移,并重复上述过程。

时间复杂度为$O(M+L)$,其中$M$是匹配文本的长度,$L$是所有匹配模式串的总长度。

计算出现次数

自动机派生树可以维护每个匹配字符串在文本串中出现的次数。在树上遍历时,每个节点的出现次数等于其所有直接子节点出现次数之和,再加上该节点自身的出现次数。这里需要特别注意,当遍历时,因为重复计数的问题,需要从上到下递归计算,而不能像多模式匹配一样从下到上计数。

时间复杂度为$O(N)$。

##代码实现

以下是自动机派生树的Python实现代码片段:

class ACNode:
    def __init__(self, id, char, parent):
        self.id = id  # 节点ID
        self.char = char  # 节点代表的字符
        self.parent = parent  # 父节点
        self.depth = 0 if not parent else parent.depth + 1  # 节点深度
        self.endpos = set()  # 末位置集合
        self.fail = None  # 失败指针
        self.next = {}  # 子节点
        self.count = 0  # 出现次数

class ACTrie:
    def __init__(self, patterns):
        self.idgen = 0  # 自增的节点ID生成器
        self.patterns = patterns  # 模式串集合
        self.alphabet = sorted(set(c for p in patterns for c in p))  # 字符集
        self.root = ACNode(self.idgen, None, None)  # 根节点
        self.nodes = {}  # 节点ID到节点实例的映射
        self.build()

    def build(self):
        # 构建自动机
        q = collections.deque([self.root])
        while q:
            u = q.popleft()
            for c in self.alphabet:
                if (v := u.next.get(c)):
                    # 子节点已存在,更新它的末位置
                    v.endpos |= u.endpos
                else:
                    # 新建子节点
                    self.idgen += 1
                    v = u.next[c] = ACNode(self.idgen, c, u)
                    self.nodes[self.idgen] = v
                q.append(v)

        # 建立失败指针
        q = collections.deque([self.root])
        while q:
            u = q.popleft()
            for c, v in u.next.items():
                if u is self.root:
                    v.fail = self.root
                else:
                    x = u.fail
                    while x and c not in x.next:
                        x = x.fail
                    v.fail = x.next[c] if x else self.root
                q.append(v)

        # 构建派生树
        for u in self.nodes.values():
            for v in u.next.values():
                # 构建父子关系
                v.parent = u
                # 加入末位置
                v.endpos |= v.parent.endpos
            # 构建子节点列表
            u.next = {c: u.next[c] for c in self.alphabet if c in u.next}

        # 计算出现次数
        self.count_patterns_in_tree()

    def count_patterns_in_tree(self):
        def dfs(u):
            for v in u.next.values():
                dfs(v)
                u.count += v.count
            u.count += len(u.endpos)
        dfs(self.root)

注意到AC自动机和自动机派生树的构建过程过程类似(只有步骤4和步骤5不同),因此完整的代码实现就不包含AC自动机的代码了。