广义后缀自动机 学习笔记
广义后缀自动机 学习笔记
概述
广义后缀自动机(General Suffix Automaton)是将后缀自动机整合到字典树中来解决对于多个字符串的子串问题。
定义
字符串集合 \(\set{s}\) 的广义后缀自动机(GSAM)是一个接受 \(\set{s}\) 的所有字符串的所有后缀的最小 DFA(确定性有限自动机或确定性有限状态机)。
记号约定
- \(s\):目标字符串。
- \(sta_0\):初始状态。
- \(\operatorname{tr}(u,c)\):状态 \(u\) 接受了字符 \(c\) 后转移到的状态。
- \(\operatorname{endpos}(t)\):一个集合,表示 \(s\) 中子串 \(t\) 的结束位置。
- \(\operatorname{link}(u)\):状态 \(u\) 对应的后缀链接(与 AC 自动机中的失配指针 \(\operatorname{fail}\) 相似)。
- \(\operatorname{longest}(u)\):状态 \(u\) 对应的字符串集合中最长子串。
- \(\operatorname{link}(u)\):状态 \(u\) 对应的字符串集合中最长子串长度。
常见的伪广义后缀自动机
-
通过用特殊符号将多个串直接连接后,再建立 SAM。
很显然这会造成匹配到无效子串、产生无效状态的问题。
-
对每个串,重复在同一个 SAM 上进行建立,每次建立前,将
last指针置零。对可能会造成对于同一个 \(\operatorname{\operatorname{endpos}(t)}\) 状态在 SAM 中有多个节点,导致出错。
离线构造
SAM Drawer (officeyutong.github.io)。
用一般 SAM 的思想进行延伸,我们可以得到 GSAM 的离线构造方法。
构建 Trie
首先第一步是构建一棵 Trie 树,只要把集合 \(\set{s}\) 中所有串都插入即可。
BFS 建立 GSAM
然后对于建完的 Trie 考虑 BFS 建立 GSAM,在 BFS 时记录当前字符和父节点在 GSAM 上的 \(lst\) 状态,然后 BFS 时类似 SAM 构造的 Extend 函数加入字符。
但是这里 GSAM 的 Extend 与 SAM 的 Extend 有所不同:
-
不创建新状态,因为我们已经离线建好了 Trie 树,于是直接在 Trie 树上跳到对应子节点即可。
还有一个细节问题,在第一次跳 \(\operatorname{link}(u)\) 更新 \(\operatorname{tr}(p,c)\) 时,由于原节点的 \(\operatorname{tr}(lst,c)\) 已经设为了 \(lst'\),所以我们要先跳一步,防止直接终止。
int p(lst); len(lst=tr[p][c])=len(p)+1,p=pa(p); while(p&&!tr[p][c])tr[p][c]=lst,p=pa(p); -
也是由于我们提前建好了 \operatorname{tr}(u,c)ie 树,那么在克隆节点的时候,可能会把一些 \(\operatorname{link}\) 比它大的给当成 \(\operatorname{tr}(u,c)\),对于这种情况,我们在克隆的时候要判断时一下 \(\operatorname{link}\)。
FOR(i,0,C-1)tr[o][i]=len(tr[q][i])?tr[q][i]:0;
那么 Extend 中其余的就没有区别了。
int Extend(int lst,const int c) {
int p(lst);
len(lst=tr[p][c])=len(p)+1,p=pa(p);
while(p&&!tr[p][c])tr[p][c]=lst,p=pa(p);
if(!p)return pa(lst)=1,lst;
int q(tr[p][c]);
if(len(q)==len(p)+1)return pa(lst)=q,lst;
int o(++tot);
len(o)=len(p)+1,pa(o)=pa(q),pa(q)=pa(lst)=o;
FOR(i,0,C-1)tr[o][i]=len(tr[q][i])?tr[q][i]:0;
while(p&&tr[p][c]==q)tr[p][c]=o,p=pa(p);
return lst;
}
考虑 BFS,不过这比较简单:
void Build() {
queue<pair<int,int> > q;
FOR(i,0,C-1)if(tr[1][i])q.push({1,i});
while(!q.empty()) {
pair<int,int> u(q.front());
q.pop();
int lst(Extend(u.first,u.second));
FOR(i,0,C-1)if(tr[lst][i])q.push({lst,i});
}
}
在线构造
引用原文链接:辰星凌的博客QAQ (cnblogs.com)。
由于离线的一般已经够用,所以我们暂时搁置。
复杂度
复杂度方面,较难证明的都在 SAM 处,而这些可以在 SAM 相关的 blog 中找到,这里不在赘述。
剩下的 BFS 和 Trie 树都是线性的不必多说。
性质
在后缀自动机上的性质绝大部分均可在广义后缀自动机上生效。
没了……
应用
GSAM 在应用方面肯定是不如 SAM 多的,比较多串的题目比较难出,而且 GSAM 结构限制较大。
本质不同子串数
例题:
最基本的,与 SAM 相同,是纯模版题。
求每个询问串是几个模版串的子串
例题:SP8093 JZPGYZ - Sevenk Love Oimaster - 洛谷 (luogu.com.cn)。
建 GSAM,然后暴力跳 \(\operatorname{link}\) 更新即可,复杂度是 \(O(n\sqrt{n})\),证明:算法记录 002: SAM 上的根号暴力 - WWW~~~ - 博客园 (cnblogs.com)(根号分治)。
或者配线段树合并可以到 \(O(n\log_2{n})\)。
字符串间的公共子串数量
例题:P3181 [HAOI2016] 找相同字符 - 洛谷 (luogu.com.cn)。
那么建出 GSAM,统计 \(siz_{i,0/1}\) 表示有多少个子串在此处,建完后拓扑累加,最后得到答案:
单用 SAM 也可以做。
多个字符串间的最长公共子串长度和数量
例题:SP1812 LCS2 - Longest Common Subs\operatorname{tr}(u,c)ing II - 洛谷 (luogu.com.cn)(长度)。
其实这个 SAM 也可以做,而且不难,复杂度也较低,为 \(O(\sum{|S|})\)。
这里的 GSAM 做法其实要好理解一点,我们如果只求长度,记一个 \(vis_{p,i}\) 数组表示是否各个串在各个节点处都有子串,那么全部建完然后再拓扑排序或一遍,最后查询每个串都有的点求最大长度即可。
如果还要求数量,那么 \(vis\) 改为 \(cnt\),定义变为出现了几次,然后拓扑排序的时候累加即可,与上一个应用相同。
设 \(n\) 表示串的总数,只查询长度的话,复杂度可以做到 \(O(\frac{n\sum{|S|}}w)\),否则是 \(O(n\sum{|S|})\) 的,没有比 SAM 的做法好。
配合数据结构
例题:CF666E Forensic Examination - 洛谷 (luogu.com.cn)。
线段树合并+GSAM。

浙公网安备 33010602011771号