📅  最后修改于: 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树,每个节点代表一种匹配。
接下来,可以按照以下过程构建自动机派生树:
endpos
集合中;fail
指针指向该节点的所有字符串的最长后缀节点;next
哈希表中;构建过程的时间复杂度为$O(N+MC)$,其中$N$是模式串总长度,$M$是模式串中相同字符的数量,$C$是字符集大小。这比起AC自动机的构建过程稍慢,但最终得到的数据结构能够支持更多的操作。
假设我们需要从一个文本串中找出所有在一个模式串集合中出现过的子串,可以按照以下步骤进行匹配:
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自动机的代码了。