后缀树(Suffix Tree)的原理与构建
1. 问题描述
后缀树是一种用于字符串处理的重要数据结构,它能够高效地解决许多字符串问题。给定一个长度为 \(n\) 的字符串 \(S\),后缀树是 \(S\) 的所有后缀的压缩字典树。它可以支持以下操作:
- 模式匹配:在 \(O(m)\) 时间内判断模式串 \(P\)(长度为 \(m\))是否是 \(S\) 的子串。
- 查找最长重复子串。
- 查找最长公共子串(在两个或多个字符串之间)。
- 查找最长回文子串。
- 等等。
后缀树的核心思想是将一个字符串的所有后缀组织成一棵压缩字典树(Trie),通过共享公共前缀来节省空间。但是,如果直接构建所有后缀的 Trie,空间复杂度将高达 \(O(n^2)\),因此需要采用更高效的算法。
2. 基础概念
2.1 后缀
对于字符串 \(S = \text{"banana"}\),其所有后缀为:
- banana
- anana
- nana
- ana
- na
- a
后缀的个数等于字符串长度 \(n\)。
2.2 压缩字典树
压缩字典树(Compressed Trie)在普通字典树(Trie)的基础上,将没有分支的路径压缩成一条边,边上标记为字符串的子串(而非单个字符)。这样可以大幅减少节点数量。
例如,对于后缀 "banana" 和 "anana",它们共享前缀 "a" 之后的 "nana",压缩后会合并路径。
2.3 后缀树的性质
- 后缀树是一棵有根树,每个边标记为 \(S\) 的一个子串。
- 每个内部节点(除根节点外)至少有 2 个子节点。
- 有且仅有 \(n\) 个叶子节点,对应 \(S\) 的 \(n\) 个后缀。
- 所有边标记的子串不重复,且拼接路径从根到叶子的标记得到完整的后缀。
- 为了确保每个后缀对应唯一的叶子,通常在 \(S\) 末尾添加一个特殊字符(如
$),保证没有后缀是其他后缀的前缀。
3. 后缀树的构建方法
构建后缀树有多种算法,最著名的是 Ukkonen 算法,它可以在 \(O(n)\) 时间和 \(O(n)\) 空间内在线构建后缀树。以下将分步骤讲解该算法的核心思想,然后说明一种直观的构建方法。
3.1 直观构建方法
为了便于理解,我们先看一个较慢但直观的方法:
- 在字符串 \(S\) 末尾添加特殊字符
$,得到 \(S' = S\$\)。 - 生成 \(S'\) 的所有后缀。
- 将这些后缀逐个插入到一棵压缩字典树中。
以 \(S = \text{"banana"}\) 为例:
- \( S' = \text{"banana\\)"} $
- 后缀列表:
- banana$
- anana$
- nana$
- ana$
- na$
- a$
- $
插入过程:
- 插入 "banana$":创建从根节点出发的一条边。
- 插入 "anana$":与现有边无公共前缀,创建新边。
- 插入 "nana$":创建新边。
- 插入 "ana\(":与 "anana\)" 共享前缀 "ana",需要拆分边,创建内部节点。
- 继续插入剩余后缀,不断拆分边以共享前缀。
最终得到的后缀树如下(简化表示):
根
├─ banana$
├─ a
│ ├─ na
│ │ ├─ na$
│ │ └─ $
│ └─ na$
├─ na
│ ├─ na$
│ └─ $
└─ $
(注:实际边标记为子串,如 "banana$"、"a"、"na" 等。)
这种方法的复杂度为 \(O(n^2)\),因为每次插入需要比较后缀与现有路径。
3.2 Ukkonen 算法核心思想
Ukkonen 算法通过在线构建和后缀链接技术将复杂度降至 \(O(n)\)。其主要步骤如下:
3.2.1 隐式树与扩展规则
算法逐个字符处理字符串,维护一棵隐式后缀树(Implicit Suffix Tree),即尚未添加特殊字符 $ 的树。在第 \(i\) 步,处理字符 \(S[i]\),将树更新为包含 \(S[0..i]\) 所有后缀的树。
扩展规则:
- 规则 1:如果路径已存在,无需操作。
- 规则 2:如果路径在某个节点结束,创建新叶子。
- 规则 3:如果路径在边中间结束,拆分边,创建新节点和新叶子。
3.2.2 后缀链接
后缀链接是从一个内部节点 \(v\)(代表子串 \(x\alpha\))指向另一个节点 \(w\)(代表子串 \(\alpha\))的指针。它帮助快速定位,避免每次从根节点重新搜索。
3.2.3 算法流程
Ukkonen 算法分为 \(n\) 个阶段(每个字符一个阶段),每个阶段有多个扩展(每个后缀一个扩展)。通过三个优化(跳过计数、快速叶子扩展、后缀链接)实现线性时间。
简要步骤:
- 初始化树为空。
- 对于每个字符位置 \(i\) 从 0 到 \(n-1\):
- 将字符 \(S[i]\) 扩展到树中,更新所有后缀。
- 利用后缀链接快速定位插入点。
- 最后添加
$完成显式后缀树。
由于算法较复杂,此处不展开细节,但需知道它能在 \(O(n)\) 时间内构建后缀树。
4. 后缀树的应用
4.1 模式匹配
给定模式串 \(P\)(长度 \(m\)),在后缀树中从根开始沿边匹配 \(P\):
- 如果匹配成功到达节点 \(v\),则 \(v\) 子树中所有叶子对应的后缀起始位置即为 \(P\) 在 \(S\) 中出现的位置。
- 复杂度:\(O(m + k)\),其中 \(k\) 是出现次数。
4.2 查找最长重复子串
在后缀树中,最深的内部节点(非叶子)对应的路径字符串即为最长重复子串。因为内部节点代表至少两个后缀共享的前缀。
4.3 查找最长公共子串
对于两个字符串 \(S_1\) 和 \(S_2\),构建 广义后缀树(将两个字符串的后缀放在同一棵树中,用不同特殊字符标记)。然后找到最深的内部节点,该节点子树中包含来自两个字符串的后缀,其路径即为最长公共子串。
5. 总结
- 后缀树是字符串所有后缀的压缩字典树,支持高效字符串查询。
- 构建方法有多种,Ukkonen 算法可在 \(O(n)\) 时间内构建。
- 应用广泛,包括模式匹配、查找重复子串、公共子串等。
- 后缀树的实际实现较为复杂,通常使用后缀数组(Suffix Array)和高度数组(LCP Array)作为替代,因为它们更节省空间且易于实现。
通过以上步骤,你应该对后缀树的原理、构建方法和应用有了清晰的理解。如果想进一步探索,可以学习 Ukkonen 算法的详细步骤或后缀数组的构建。