后缀自动机-SAM
link to cnblog
基础模型
初学 SAM,感觉这东西实在是抽象...
建议就是感性理解 + 背板子
参考资料:
OI-Wiki
cmd 的博客
\text{endpos}
首先我们来了解什么是
换句话而言,就是两个
-
- 若
q 为p 的后缀,那么有\text{endpos}(p)\subseteq \text{endpos}(q)
- 若
-
- 对于一个等价类(就是同一种
\text{endpos} 的所有子串),将字符串按长度从小到大排序后,前一个字符串始终为后一个字符串的后缀,且长度是连续的
- 对于一个等价类(就是同一种
parent tree
上面
对于一个等价类,设它最长的字符串为
但显然,新串对应的等价类是原串等价类的子集
因此我们可以利用这个子集关系建立一棵树,称为 parent tree
其实这可以看成将一个等价类分裂成若干个类,然后成为原等价类的儿子
parent tree 中,由儿子指向父亲的链成为后缀链接
那它与 SAM 有什么关系呢?
其实,SAM 上的结点意义与 parent tree 的意义相同
而且可以证明,依靠 parent tree 建立出来的 SAM,结点数(自动机状态)不超
下面是 SAM (左)与 parent tree (右)的对比(原串:"abcbc")
(其中绿色结点表示终止链上的结点)
构建 SAM
先放代码,逐层分析:
struct Node
{
int nxt[26], lk, len;
}a[N << 1 | 5];
int tot, la;
inline void sam_expand(int c)
{
int now = ++tot, p = la;
a[now].len = a[p].len + 1;
for(; ~p && !a[p].nxt[c]; p = a[p].lk) a[p].nxt[c] = tot;
if(p == -1) a[now].lk = 0; // case 1
else
{
int q = a[p].nxt[c];
if(a[p].len + 1 == a[q].len) a[now].lk = q; // case 2
else
{
int nq = ++tot;
a[nq] = a[q], a[nq].len = a[p].len + 1;
for(; ~p && a[p].nxt[c] == q; p = a[p].lk) a[p].nxt[c] = nq;
a[now].lk = a[q].lk = nq;
} // case 3
}
la = now;
}
变量解析:
struct Node
{
int nxt[26], lk, len;
}a[N << 1 | 5];
int tot, la;
nxt[] 表示自动机的转移,lk 表示后缀链接,len 表示这个等价类最长的字符串的长度
tot 表示结点(状态)个数,la 表示终止链的位置
最开始我们要初始化空的根节点
a[0].lk = -1
现在每次在串最后增加一个字符:
int now = ++tot, p = la;
a[now].len = a[p].len + 1;
新建一个结点
for(; ~p && !a[p].nxt[c]; p = a[p].lk) a[p].nxt[c] = tot;
if(p == -1) a[now].lk = 0; // case 1
我们将终止链上状态都转移到现在的状态,这样就加入了新的后缀
如果 p=-1,代表新后缀在之前都没出现过,也就不是任何等价类的子集,因此后缀链接指向根节点
else
{
int q = a[p].nxt[c];
if(a[p].len + 1 == a[q].len) a[now].lk = q; // case 2
else
{
int nq = ++tot;
a[nq] = a[q], a[nq].len = a[p].len + 1;
for(; ~p && a[p].nxt[c] == q; p = a[p].lk) a[p].nxt[c] = nq;
a[now].lk = a[q].lk = nq;
} // case 3
}
否则,代表之前的串就已经出现了新后缀的状态,我们要分两种情况讨论(q 就是出现过的新后缀的状态位置):
-
q的等价类只有一个串:那么这个串就是新后缀,因此直接将等价类扩展即可,将后缀链接指向q -
否则,
q的等价类大小应该小于新后缀对应的等价类,因此我们要将它替换掉:
-
我们新建一个状态
nq,将q中除了len复制到nq中,而nq.len=p.len+1 -
接着,继续遍历终止链,将原来指向
q的状态,改为指向nq -
最后,将
q和新后缀的等价类的后缀链接指向nq
这样能保持 SAM 与 parent tree 的性质
模板题
显然,每个串的
我们可以在 parent tree 上跑树形 dp,计算 u 子树内
注意
代码