题解:P3804 【模板】后缀自动机(SAM)
SAM 学习笔记
简介:
SAM 是一个用于维护子段信息的自动机。
当然,你也可以把它看成是一个插入了所有后缀的 trie。
铺垫:
我们定义
字符串
给个例子:令文本串
-
ed(\text{a}) = \{ 1, 3, 6 \} -
ed(\text{ba}) = \{ 3, 6 \} -
ed(\text{aba}) = \{ 3\} -
ed(\text{bba}) = \{ 6\} -
ed(\text{b}) = \{ 2, 4, 5 \} -
ed(\text{ab}) = \{ 2, 4\} -
ed(\text{bab}) = \{ 4 \}
其他先不列出来了,我们观察一下这个有什么规律吧。
-
如果一个字符串
s 是一个字符串t 的后缀, 那么有ed(s) \supseteq ed(t) 。口头证明一下:每一次字符串
t 出现,那么s 一定出现,但是s 可能有出现了但是t 没有完全出现的情况,所以成立。 -
如果
ed(s) \supsetneq ed(t) ,那么字符串s 是一个字符串t 的后缀。其实几乎是 命题1 的逆命题,如果每一次
t 出现都一定有对应s 的出现,那么显然就一定是后缀。 -
要么有
ed(s) \supseteq ed(t) 或ed(s) \supseteq ed(t) ,要么有ed(t) \cap ed(p) = \varnothing .考虑反证:假设现在有交集,那么对于一个共同元素
p ,考虑以p 结尾的所有串互为前后缀关系,由 命题1 即可知道一定存在包含关系。 -
对于若干个
s_i ,若其ed(s_i) 相等,那么其长度构成一个连续的区间。考虑证明:首先它们互为后缀关系,那么假设第一次让
ed(p) 扩大的点k ,当k' \ge k +,显然不会回退,得证。
引入:
我们考虑我们想要把所有后缀插入到一个字典树中:
注意到:这些后缀的相同前缀我们已经通过字典树的结构来合并了,那么相同后缀怎么解决呢?
由这个 命题1 想到我们可以通过
现在看到我们的
证明: 由 命题3 得知,从空集开始,每一次增加都必然减少一个块,所以是
那么我们考虑转移是什么?
首先我们想到的是像字典树的 next 一样的,从一个
然后我们考虑这个不仅仅是一个 trie,还具备子串匹配功能:那么我们需要维护一个像 AC自动机 的 fail 一样的东西,在这里我们叫他 link。
我们观察 AC自动机 的 fail 功能是什么?
fail表示AC自动机上与当前节点代表的字符串具有相同后缀的最长字符串对应节点。
我们直接 copy 过来,然后简单修改一下:
根据 命题4 :每个节点都一定对应一段长度连续的后缀。
那么我们取出其中最长的一个串作为代表:
link 表示 SAM 上与当前节点的代表字符串具有相同后缀的最长代表字符串对应节点。
那么我们的结构就出来了:
如图就是字符串 next 的构造:
link 的构造:
构建
观察一下这个图片,我们总结一点规律:
- 首先我们从上一次增加的最后一个点
lst开始: -
不断向上跳
link,注意到这样其实就是在枚举若干种可能的结尾:link是具有相同后缀的最长串节点 -
考虑跳
link的过程中,当前跳到p :- 如果
p 没有对应的next[c]:我们可以直接将next[c]连接到当前点。 -
如果这个点有对应的
next[c],那么显然前面的都有next[c]。-
如果此时恰好每一个
p 中的串都可以通过加一个c 跳转到next[c]中的一个串,那么显然就可以讲当前点的link连向next[c]。这可以通过比较两者的长度来完成。
-
否则就是
next[c]有部分串无法通过加入一个c 从p 跳转到next[c],那么我们就将两部分划分出来,新建一个克隆节点q 表示可以跳转的部分。接下来我们需要向前继续跳
link来把原本连接向next[c]的部分连接到q 。
-
- 如果
那么我们的构建就讲完了:过程比较复杂,大家可以对着图片多手算几次。
实现
void add (int c) {
int now = np(), p;
t[now].len = t[lst].len + 1;
for (p = lst ; ~p && !t[p][c] ; p = t[p].lnk)
t[p][c] = now;
if (p == -1) t[now].lnk = 0;
else {
int pre = t[p][c];
if (t[pre].len == t[p].len + 1) t[now].lnk = pre;
else {
int ppre = np (pre);
t[ppre].len = t[p].len + 1;
t[pre].lnk = t[now].lnk = ppre;
for ( ; ~p && t[p][c] == pre ; p = t[p].lnk)
t[p][c] = ppre;
}
}
ans += t[now].len - t[t[now].lnk].len;
lst = now;
}
应用
-
P3804 考虑长度已经记录了,我们怎么对于一个节点求其出现次数:
发现出现次数
= ed(p) 的大小:这个东西怎么维护?在加入一个点的时候将这个点设为 1, 那么一个点的
ed(p) 大小就可以认为是子树和。最后枚举节点取
\max 即可。