子序列自动机:原理、构建与应用
1. 什么是子序列自动机子序列自动机Subsequence Automaton是一种用于高效处理字符串子序列匹配问题的数据结构。给定一个文本串S长度n子序列自动机可以在O(|T|)的时间复杂度内判断任意一个模式串T是否是S的子序列而无需每次重新扫描整个S。这里“子序列”的定义是从原字符串中删除零个或多个字符后保持剩余字符相对顺序不变所形成的新字符串。例如“ace” 是 “abcde” 的子序列。2. 核心原理与构建子序列自动机的核心思想是预处理文本串S构建一个状态转移表next[i][c]表示从位置i开始下一个字符c出现的最早位置。如果不存在则记为n或一个特殊值。构建过程通常采用从后向前扫描的动态规划方法// 假设字符集大小为 26小写字母 vectorvectorint buildSubsequenceAutomaton(const string s) { int n s.length(); vectorvectorint next(n 1, vectorint(26, n)); for (int i n - 1; i 0; --i) { next[i] next[i 1]; next[i][s[i] - a] i; } return next; }这样对于任意模式串T我们可以从pos 0开始依次查找next[pos][T[j]]如果某次查找到n则说明T不是S的子序列。3. 时间复杂度与空间复杂度预处理构建O(n * |Σ|)其中|Σ|是字符集大小。单次查询O(|T|)与文本串长度n无关。空间复杂度O(n * |Σ|)可以通过滚动数组优化到O(|Σ|)但查询时需要从后向前重新计算牺牲查询时间。4. 典型应用场景多模式子序列匹配需要频繁判断大量短串是否为某个长串的子序列时预处理一次每次查询O(|T|)。字符串包含性检查在编辑距离、DNA序列比对等场景中快速判断一个串是否“包含”另一个串作为子序列。动态规划优化某些 DP 问题中状态转移需要查找下一个特定字符的位置子序列自动机可以加速这一过程。竞赛编程题目如 Codeforces、LeetCode 中涉及子序列匹配的题目。5. 代码示例完整可运行#include iostream #include vector #include string using namespace std; class SubsequenceAutomaton { private: vectorvectorint next; int n; public: SubsequenceAutomaton(const string s) { n s.length(); next.assign(n 1, vectorint(26, n)); for (int i n - 1; i 0; --i) { next[i] next[i 1]; next[i][s[i] - a] i; } } bool isSubsequence(const string t) { int pos 0; for (char c : t) { if (pos n) return false; pos next[pos][c - a] 1; if (pos n) return false; } return true; } }; int main() { string s abcde; SubsequenceAutomaton sa(s); cout sa.isSubsequence(ace) endl; // 1 (true) cout sa.isSubsequence(aec) endl; // 0 (false) return 0; }6. 与其他数据结构的对比数据结构预处理时间单次查询时间适用场景子序列自动机O(n * |Σ|)O(|T|)多次子序列匹配双指针扫描无O(n |T|)单次查询后缀自动机O(n)O(|T|)子串匹配、更多复杂操作Trie 树O(总长度)O(|T|)前缀匹配、字典查询7. 总结子序列自动机是一种用空间换时间的典型数据结构适用于文本串固定、需要频繁进行子序列匹配的场景。虽然其空间开销相对较大但在字符集较小如小写字母、数字或文本串长度可接受时能带来显著的查询效率提升。在算法竞赛和某些特定字符串处理任务中它是一个值得掌握的工具。