后缀树
SA 建后缀树
与单调栈构建虚树的原理完全相同:link
- 可以利用后缀排名代表 dfs 序,因为是按照字符字典序升序 dfs 后缀树。于是单调栈的整个过程等价于 dfs 这棵后缀树,单调栈内保存的始终是后缀树上根节点到当前结点的右链中的所有结点。
- SA 的 height 数组恰好表示两个排名相邻后缀的 lcp 长度,正好对应虚树中的 lca 深度。因此可以直接通过比较结点深度与 height 大小进行单调栈的入栈与出栈过程。
- 若不存在相应深度的结点,说明当前后缀树中不存在 lca 结点,需要额外新建一个 lca 结点;
- 否则当前栈顶结点就是 lca,无需额外新建结点
- 后缀树的每个结点需要维护其加权深度:
- 叶节点的深度就是其代表后缀的长度
- 非叶节点的深度代表其子树内所有叶节点代表后缀的 lcp 长度
- 每个结点还需要维护其代表子串(叶节点是后缀,非叶节点是子串)在原串的起始下标。
- SA 构建后缀树相比 SAM 更直观(不存在字符串反转问题)。形成的是压缩后缀树,与暴力将所有后缀插入形成的字典树的唯一区别在于:压缩后缀树中的边体现为原字典树中一条长度不定的链(包含 \(m\) 条字符边与 \(m-1\) 个结点)
- 注意压缩后缀树不一定恰好有 \(n\) 个叶节点, 而是 \(\le n\),可能存在某些后缀体现在非叶节点。
- 复杂度分析:考虑原字典树,从插入第二个后缀开始,插入的字符串一定体现为前面某一部分已有字符边,后面剩下的部分不存在字符边,因此在原字典树中每次插入最多只会增加一条链。在压缩后缀树中可能会涉及到压缩边的分裂,但每次最多只会有一条压缩边分裂成两条压缩边,故每次最多新增 \(2\) 个结点,总结点数不超过 \(2n\)。
模板
struct SuffixTreeNode{
int dep, pos; // 深度,起始下标
vector<int> child; // 邻接表
};
vector<SuffixTreeNode> tree(2 * n + 5);
vector<int> stk;
int tot = 1, root = 1;
tree[root] = {0, SA.sa[1], {}};
stk.push_back(root);
int first_leaf = ++tot;
tree[first_leaf] = {n - SA.sa[1] + 1, SA.sa[1], {}};
tree[root].child.pb(first_leaf);
stk.push_back(first_leaf);
for(int i = 2; i <= n; i ++){ // 新加入表示后缀 s[i~n] 的叶节点
int h = SA.height[i];
int last = 0; // 最后一个弹出栈的结点编号
while(!stk.empty() && tree[stk.back()].dep > h){
last = stk.back();
stk.pop_back();
}
int parent = (!stk.empty() ? stk.back() : 0);
if(tree[parent].dep < h){ // lca 不在栈内
int mid = ++tot; // 新建一个结点,为 lca(last, leaf)
tree[mid] = {h, tree[last].pos, {}};
tree[parent].child.pop_back(); // 弹出的结点一定是 last
tree[parent].child.pb(mid);
tree[mid].child.pb(last);
stk.push_back(mid);
parent = mid;
}
// 否则 parent 就是 lca
int leaf = ++tot; // 加入叶节点
tree[leaf] = {n - SA.sa[i] + 1, SA.sa[i], {}};
tree[parent].child.pb(leaf);
stk.push_back(leaf);
}
会发现新建 lca 结点 mid,插入到 parent 与 last 之间这一行为,与 SAM 中 len 的划分很相似。
SAM 建后缀树
建立反串的 SAM 即可

浙公网安备 33010602011771号