SAM与咸化
就本着认真负责的态度来一点 SAM 咸化吧。
其实是杜教筛推不动了来划水了。
刚开始学 SAM 的时候,翻遍了各种博客和题解,但是都没有看太懂。
直到后来去借助某可视化网站,一点一点去看,才懂了一点。
于是,想着来搞一个不一定详细但是适合入门的 SAM 咸化。
放上SAM可视化
注意:本蒟蒻对 SAM 的理解非常浅,写错了那就错了吧。
本文章并不会证明 SAM 的各种性质,所以请巨佬们忽略本文。
- 什么是 SAM
是后缀自动机。
此部分完结。
(下面的内容含有不严谨成分,后面再说明)
可以类比一下AC自动机,就是一张基于trie树的图。
根据名字可知,这个自动机只接受相应字符串的后缀。
然而事实上不是这样。我也不知道为啥这东西叫后缀自动机。
这个自动机的性质是,它可以接受相应字符串的任意子串。
换句话说,从根节点开始的任意一条路径,都是字符串的一个子串。
我们记字符串长度为
SAM 的一个优秀性质是,节点个数不会超过
给一些前置约定。
SAM的每个节点包含三样东西:
则
于是我们有,
要是再没懂建议好好再看看。
然后我们说完了
那就扔了吧我们去引入别的概念。
为了方便使用,我们再引入几个小结论:
-
对于
S[i,j] 以及其后缀S[k,j]\ (i < k \le j) ,我们有endpos(S[i,j]) \subseteq endpos(S[k,j]) 如果要感性证明一下的话,我们可以发现
S[i,j] 出现过的位置S[k,j] 都出现了,而S[k,j] 出现过的位置S[i,j] 不一定出现,所以是包含关系。 -
两个不同子串的
endpos 要么是包含关系要么就不相交。
感性证明的话考虑反证。如果子串
又根据结论
- 一个
endpos 集合可以表示一些后缀相同、长度连续的子串。
严谨的讲,一个
感性证明就免了吧。直接根据结论
总是要自行思考的,我就不多嘴了。
就先有这三个结论就够了。
然后我们来换一个角度理解 SAM:
首先,每个节点表示一个
当然,我们并不维护其
关于
一个节点表示的子串里面,最长的长度我们记为
关于
之前我们说过
(在这里,我们就感性的认为一个集合比它的子集大好了。。。)
有点抽象?我们换一种说法。
随手掏出来一个子串
那么同样的,根据结论
那么就会有另一个节点去表示
于是我们有,
因此考虑一下跳
既然跳
具体性质后面再说。
关于
走一条边其实是相当于在后面添加一个字符。这个其实和AC自动机是一样的。
另外,
一个额外的性质:
有了这两个性质,我们就可以用桶排序来代替拓扑排序了。
只需要对
更好的是,
好了,磨叽了这么半天,终于可以开始着手构建了。
我们提供的构建方法是在线的,可以一个字符一个字符的插入。
在最开始,我们有一个空节点
注意,并不存在节点
首先,假设你已经完成了
- 记上次构建结束的位置为
last ,则我们申请一个新的节点now ,并且让len(now) = len(last) + 1 。
这一步还挺显然的,就是新增加一个节点。
- 接下来搞一个指针
p = last ,然后p 不断往link(p) 的位置跳,同时置p.edge[S[i]] = now ,直到p = 0 或者p.edge[S[i]] \neq 0 就停止。
这里,我们有必要仔细考虑一下这奇怪的操作是在干啥。
首先,
而
那么我们置
同时,现在会有
既然现在出现了那总是要有一个节点来存吧。
我们发现
- 如果
p = 0 了,让link(now) = 1 ,然后结束构建。
这一步就是,我们发现
节点
- 如果
p \neq 0 ,我们记q = p.edge[S[i]] ,然后开始后面的分类讨论。
值得一提的是,这里的
真的是这样吗。。。
- 若
len(q) = len(p) + 1 ,则置link(now) = q ,然后结束构建。
好,确实是这样。
看样子 SAM 建起来还是挺好理解的。
-
若
len(q) \neq len(p) + 1 ,则新建一个节点ka ,ka 直接复制q 的信息,同时让len(ka) = len(p) + 1 。 -
然后,找一个指针
h = p ,然后再让h 往link 跳,同时置h.edge[S[i]] = ka ,直到h.edge[S[i]] \neq q 或者h = 0 为止。 -
最后,置
link(now) = link(q) = ka ,结束构建。
这里的操作比较密集并且完全看不懂在干什么。
好了后面不会了本文结束。
后面提供一份代码然后我就跑路。
注意,后面建议慢点读,开始出现数字了
首先我们回忆一下前面都干了点啥。
从
我们记跳到
也就是说现在
(注意,
接下来,发现
那么我们显然不能再把这些点划分到
但,现在,我们发现
则存在另一种子串,它和
现在显然不能直接把
但是现在
所以可以想到的是,我们应该把
我们记
发现正好有
顺便,我们的目的也达成了,可以直接置
但是这就结束了吗?
并没有。
我们发现,我们本来是指望着找到
换句话说,从
现在我们把长度
当然,顺着
于是乎,我们需要去遍历
之前学的时候我还有另一个疑惑,为什么不用
这样一来,不就不需要再去遍历
而实际上非常不可以。
可能会存在一些节点
也就是说,如果我们要让
而这显然很难实现。复杂度也不对。
- 最后,记得置
last = now 。
这个没啥好说的。
以上内容没有看懂的话没关系,建议自己多思考,可以再看一遍。
理解 SAM 构建可能对你做题没有太大的帮助,但是还是建议理解一下。
终于,我们的 SAM 建完啦!
完结撒小花~
这里,附上一份我自己打的板子。
(其实各个 SAM 板子基本都类似,自己打一份能记住就行)
#include <map>
#define sz 100005
using namespace std;
struct site
{
int len, link;
map<int, int> net;
};
struct site tree[sz << 1 | 1];
int last = 1, top = 1;
void add(int a)
{
int now = ++top;
tree[now].len = tree[last].len + 1;
for ( ; last && (!tree[last].net[a]); last = tree[last].link)
tree[last].net[a] = now;
if (!last)
tree[now].link = 1;
else
{
int q = tree[last].net[a];
if (tree[q].len == tree[last].len + 1)
tree[now].link = q;
else
{
int ka = ++top;
tree[ka] = tree[q];
tree[ka].len = tree[last].len + 1;
for ( ; last && tree[last].net[a] == q; last = tree[last].link)
tree[last].net[a] = ka;
tree[now].link = tree[q].link = ka;
}
}
last = now;
}
这里,我存
当字符集大小只有
先写到这里吧。。。
关于 SAM 及其
(内容会比上面的多)
到这里已经写了将近
虽然可能更不好看懂的样子。
希望可以对后几届的学长们有用吧。
因为自己学 SAM 真的是太难了。。。
补充一点东西。
我们有经典结论,反串 SAM 的
所以,对于 SAM 上的某个点,指向这个点的
并且这个上界是可以卡满的。
所以,对于字符集为 26 的字符串,我们可以考虑在新建
总体复杂度
当然,因为需要多记录一些东西所以时空常数会大一点。
顺便,这种写法还是有一些好处的。
容易发现这样子建出来的 SAM 满足
需要在