SAM学习笔记
\(SAM\)(后缀自动机)是一种强力的关于字符串的自动机
它所包含的边有两类。
第一种边构成的集合是一个 \(DAG\) 图,这个图的每个边代表一个字符,就跟 \(Trie\) 树的感觉差不多,从源点 \(t_0\) 沿这个 \(DAG\) 图走到任意点所构成的一个字符串都是原字符串的子串,如果走到不能走了,那构成的字串就是原串的一个后缀(原串的后缀不一定非得从 \(t_0\) 走到不能走为止才是)
第二类的边构成的一个集合是一个树,我们称它为 \(Parent\) 树,这棵树的父亲节点是儿子节点的一个后缀,叶子节点一定是原串的一个前缀(同上,原串的前缀不一定是叶子节点)
\(SAM\) 中,每个点代表了一些字符串(就是从 \(t_0\) 通过边走到这个点所构成的字符串),不难发现,每个点至少代表一个字符串,而且它所代表的字符串长度一定是一个区间,并且短的都是长的的后缀

接下来讲怎么建 \(SAM\)
首先说明需要维护哪些东西:\(f_i, fa_i, len_i, ch_{i,c}\),\(f_i\) 表示这个代表的这些串在字符串中出现了几次,\(fa_i\) 表示这个点在 \(Parent\) 树上的父亲,\(len_i\) 表示这个点表示的所有串中最长的串的长度,结合 \(Parent\) 树上它的父亲长度以及 \(DAG\) 图上的路径其实就能得出它都代表哪些串,\(ch_{i,c}\) 表示这个点在 \(DAG\) 图上的出边都有啥
构建的过程是在线的,我们考虑在一个串的末尾加一个字符 \(c\),设这个串是 \(S\),考虑怎么在 \(trie\) 树上添加这个字符
首先,它在 \(DAG\) 图上肯定是在上一个添加的点 \(p\) 后面再添加一个 \(c\) 的边,然后指向这个新建的点 \(np\),然后肯定是跳 \(p\) 在 \(Parent\) 树上的点,因为这些点是 \(p\) 的后缀,所以只可能它们连向这个点 \(np\)
如果一直跳到头了,那就说明之前的每个后缀都没有对于这个字符的连边,也就是说这个字符串中还没有这个字符(这里可以自己理解理解),所以就把 \(fa_{np}\) 连向 \(t_0\) (也就是1)
如果跳到一个点了,但是这个点在 \(DAG\) 图上有一条为字符 \(c\) 的出边了,那怎么办呢
那我们直接看这个 \(ch_{p,c}\) 连的边所指向点 \(q\) 的 \(len_q\) 是不是等于 \(len_p+1\),如果等于的话我们就让 \(fa_{np}=q\),因为 \(q\) 长度就比 \(p\) 大1,所以 \(q\) 点代表串只有一个,就是 \(p\) 代表的最长串后面加了个字符 \(c\),那这个 \(q\) 点代表的串显然是你现在整个串 \(S\)(也就是 \(np\) 这个点代表的最长串)的一个后缀,并且一定是最长的后缀,所以我们只需要让 \(np\) 在 \(Parent\) 树上的父亲节点指向 \(q\) 就行了。
那如果比 \(len_p+1\) 大呢?
那就表明这个 \(q\) 点不仅包含了现在的串 \(S\) 的后缀还包含了别的不是后缀的东西,所以我们需要把 \(q\) 点拆出来就行了,具体来说就是拆成 \(S\) 的最长后缀和别的包含的串两个点,然后分别在 \(DAG\) 和 \(Parent\) 数上连边就行了
void SAM (int x) {
int p = lst, np = lst = ++tot;
f[np] = 1, len[np] = len[p] + 1;
for (; p && !ch[p][x]; p = fa[p])
ch[p][x] = np;
if (!p) fa[np] = 1;
else {
int q = ch[p][x];
if (len[q] == len[p] + 1) fa[np] = q;
else {
int nq = ++tot;
for (int i = 0; i < 26; ++i)
ch[nq][i] = ch[q][i];
fa[nq] = fa[q], len[nq] = len[p] + 1;
fa[np] = fa[q] = nq;
for (; p && ch[p][x] == q; p = fa[p])
ch[p][x] = nq;
}
}
}

浙公网安备 33010602011771号