SAM 学习笔记

一、Parent Tree 和 SAM 的基本结构

parent tree 和 SAM 共用节点,每个节点代表 endpos,parent tree 是一棵树,SAM 是 DAG。

parent tree 向儿子走相当于往前加字符,SAM 上向儿子走相当于往后加字符。

SAM 上一条路径与字符串的所有本质不同的子串一一对应,路径的终止节点代表了该子串的所有出现位置。

代码中 fa 记录 parent tree 上的父亲,ch 记录 SAM 上的儿子。

二、endpos 的含义

endpos 是一个集合,是一个由子串结束位置构成的集合。endpos 大小表示这些字符串的出现次数,我们设 \(f_u\) 表示节点 \(u\) 的 endpos 集合大小。

在 parent tree 上, \(\sum f_{son_u} \leq f_u \leq \sum f_{son_u}+1\),且父亲的 endpos 一定包含儿子的 endpos。

endpos 大小“丢失”问题:指父亲的 endpos 中某个结束位置在所有儿子中均未出现。这个丢失的串一定是前缀。

解释:parent tree 向下走表示往前添加字符,前缀无法在前面添加字符,所以产生丢失。

丢失发生的位置:叶子节点以及其他包含前缀串的节点。

叶子节点一定代表前缀。

三、endpos 的子串长 len

如果仅仅知道子串的末尾位置,不足以让我们知道子串的内容,还需要知道子串的长度,所以我们记录 len

len 表示该节点记录的所有子串中,最长子串的长度。parent tree 上的 len[fa]+1 为该点的最短串长度。

SAM 上儿子节点的 len 至少为父亲节点 \(+1\),因为任何一条到达父亲的路径都能通过添加一个字符到达儿子,而儿子的父亲可能有多个(因为 SAM 是 DAG)。

为何 len[fa]+1 为该点的最短串长度?

反证,如果不是最短,那么长度为 len[fa]+1 的串将不属于任何一个 endpos,不合理。

四、构建 SAM

【模板】后缀自动机(SAM)

核心思想是维护 SAM 的性质,即 parent tree 上的父亲,SAM 上的儿子,每个节点的最长 len。

代码中根为 \(1\),根代表空串。

\(n\) 为插入新的字符后字符串的长度,\(x\) 为插入的字符。

Part 1 找到 endpos 被修改的节点

除新建节点外,第一个 endpos 被修改的节点,就是新建节点的父亲。

//lst 表示 endpos 为 n-1,len 为 n-1 的节点,即子串[1,n-1]的节点。
//now 新建的节点
int p=lst;
int now=lst=++tot;len[now]=len[p]+1;
while(p&&!ch[p][x]) ch[p][x]=now,p=fa[p];
if(!p) fa[now]=1;
else{
  //////
}

新建:当我们新添加一个字符后,我们得到一个 endpos 为 \(n\),len 最长为 \(n\) 的节点。

跳父亲:之后我们从节点 \([1,n-1]\) 开始在 parent tree 跳父亲,这等价于不断跳更短的后缀,直到找到一个存在儿子 \(x\) 的节点,设其为 \(u\)

终止:此时 \(u\) 的 endpos 集合中应当加入 \(n\) 这个位置,我们找到了 endpos 被修改的节点。

修改:如果跳到了根,说明前面不存在一个 endpos 会加入 \(n\) 的节点,那么它的父亲一定是根,因为只有根的 endpos 包含了 \(n\)

跳到了根代表的更多含义:

该字符在字符串中第一次出现,因为如果不是第一次出现,必然存在一个非根节点的 endpos 发生改变,与事实相违。

新建的节点 \(now\) 代表了所有后缀子串,因为它的父亲为根,根的 len\(0\)

Part 2 不分裂节点修改 endpos

if(!p) fa[now]=1;
else{
  int q=ch[p][x];
  if(len[q]==len[p]+1){
    fa[now]=q;
    return;
  }
  //////
}

我们能否直接把 \(n\) 加入,取决于 \(q\) 节点包含的子串是否与 \(p\) 节点添加 \(x\) 字符后的子串完全相同,如果相同,就可以直接将 \(n\) 加入 \(q\) 的 endpos,然后修改 \(now\) 的父亲为 \(q\)

Part 3 分裂节点修改 endpos

int q=ch[p][x];
if(len[q]==len[p]+1){
  //////
}
int nq=++tot;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];len[nq]=len[p]+1;

fa[q]=fa[now]=nq;
while(p&&ch[p][x]==q)ch[p][x]=nq,p=fa[p];

分裂节点:是因为节点 \(q\) 中长度 \(\leq\) len[p]+1 的部分 endpos 中添加了 \(n\),而长度更大的未加入,所以分裂节点,维护 endpos 的性质。

新建 \(nq\) 节点:在它的 endpos 中加入 \(n\),由于 parent tree 上满足父亲 endpos 包含儿子。所以父子关系可表示为 \(q\rightarrow nq\)\(now \rightarrow nq\)

跳父亲:\(p\) 的一些祖先仍然指向 endpos 不含 \(n\) 的节点 \(q\),我们需要加入 \(n\),只需将它们的儿子修改为 \(nq\) 即可。

Part 4 一些现象

SAM 上许多节点 endpos 的修改是隐式的,比如所有 \(nq\) 的祖先,但是它们包含后代 \(nq\),所以就隐式添加了。我们只有在必要时,才去修改 endpos。

五、求 endpos 的大小

因为丢失 endpos 的一定是包含前缀的节点,所以直接在新建前缀节点的让它的点权 \(+1\),那么一个点的 endpos 大小就是子树内的点权和。

具体可参见模板题。

posted @ 2025-05-09 11:33  born_to_sun  阅读(28)  评论(0)    收藏  举报