对于鱼来说,写过模板而不写博客的后果就是过了几个月跟没学过一毛一样。
所以要开始营业了
Part 1 一些基本定义
- e n d p o s ( t ) endpos(t) endpos(t):字符串 t t t 在 s s s 中出现的所有结尾位置的集合
- 等价类:若 e n d p o s ( x ) = e n d p o s ( y ) endpos(x)=endpos(y) endpos(x)=endpos(y),则 x x x 和 y y y 属于一个等价类。以下称对应同一等价类的所有子串为一个状态。
- l i n k ( x ) link(x) link(x):根据 e n d p o s endpos endpos 的性质,状态 x x x 对应的所有子串都是其中最长的子串 t t t 的后缀,而 t t t 的后缀不一定都属于状态 x x x。规定 t t t 的最长的不属于 x x x 的后缀为 l i n k ( x ) link(x) link(x)。
那么 S A M SAM SAM 就是一个以状态为点,状态之间的 l i n k link link 转移为边的 DAG,从起始状态出发到达每一个终止状态之间的转移构成 s s s 的一个后缀。
Part 2 构建
S A M SAM SAM 的构建是 O ( n ) O(n) O(n) 的线性算法,通过逐个加入每个字符来实现,节点数最多只有 2 n − 1 2n-1 2n−1。
每个节点包含这样几个基本信息:
- l e n [ x ] len[x] len[x]: x x x 对应的最长字符串长度
- f a [ x ] fa[x] fa[x]:即 l i n k ( x ) link(x) link(x)
- c h [ x ] [ i ] ch[x][i] ch[x][i]:记录 x x x 的转移边上对应字符为 i i i 的后继状态
考虑插入字符 x x x 的过程,假设上一个插入的节点是 p p p,当前新建的节点是 n p np np。
先令 l e n [ n p ] = l e n [ p ] + 1 len[np]=len[p]+1 len[np]=len[p]+1,从 p p p 往它的祖先跳,直到跳到某个节点有对应字符 x x x 的转移边或者跳到初始状态,这一路都是没有 x x x 的转移边的,直接令 c h [ p ] [ x ] = n p ch[p][x]=np ch[p][x]=np。
分情况讨论:
-
到初始状态还没有找到 x x x 的转移边,直接令 f a [ n p ] = fa[np]= fa[np]=初始状态;
-
找到了 x x x 的转移边,假设转移到了点 q q q,即 c h [ p ] [ x ] = q ch[p][x]=q ch[p][x]=q:
- l e n [ q ] = l e n [ p ] + 1 len[q]=len[p]+1 len[q]=len[p]+1,即字符串上点 p p p 和点 q q q 相邻,根据后缀状态的定义,令 f a [ n p ] = q fa[np]=q fa[np]=q 即可;
- l e n [ q ] > l e n [ p ] + 1 len[q]>len[p]+1 len[q]>len[p]+1,即 q q q 对应着比起点到 p p p 更长的子串,这时候将 q q q 割裂成两个节点 q q q 和 n q nq nq,使 l e n [ n q ] = l e n [ p ] + 1 len[nq]=len[p]+1 len[nq]=len[p]+1, n q nq nq 中保留 q q q 的其它转移,让 f a [ q ] fa[q] fa[q] 和 f a [ n p ] fa[np] fa[np] 全为 n q nq nq,再修改 p p p 到初始状态上的所有沿边 x x x 转移会到达 q q q 的后继状态为 n q nq nq。
然后我们就得到了一个可爱的 S A M SAM SAM。
c o d e code code:
struct qwq{
int len,ch[26],fa;
}st[N];
int tot=1,lst=1;
inline void insert(int x){
int p=lst,np;
np=lst=++tot,st[np].len=st[p].len+1,f[np]=1;
for(;p&&!st[p].ch[x];p=st[p].fa) st[p].ch[x]=np;
if(!p) return st[np].fa=1,void();
int q=st[p].ch[x];
if(st[q].len==st[p].len+1) return st[np].fa=q,void();
else{
int nq=++tot;st[nq]=st[q],st[nq].len=st[p].len+1;
st[np].fa=st[q].fa=nq;
for(;p&&st[p].ch[x]==q;p=st[p].fa) st[p].ch[x]=nq;
}
}
Part 3 一些基础应用
求本质不同的子串个数
一种方法是利用 DAG 的性质求从起点出发的路径条数,更好写的是利用树的性质,每个节点对应的子串数量为 l e n [ x ] − l e n [ f a [ x ] ] len[x]-len[fa[x]] len[x]−len[fa[x]],但要注意计入答案的应该是当前节点刚插入自动机时的 f a fa fa,且后续割裂的 n q nq nq 不计入贡献。
求第k小子串
不同位置的相同子串算一个:
每个本质不同的子串唯一对应 DAG 上一条以 1 1 1 为起点的路径,建出 S A M SAM SAM 之后,自动机上除起始点外每个点权值为 1 1 1,反图上拓扑dp求出每个点出发能走的路径条数。
不同位置的相同子串算多个:
同样建出 S A M SAM SAM,后复制的点不算权值,在 p a r e n t parent parent 树上对每个点的子树进行求和,得到 e n d p o s endpos endpos 的大小作为点的权值,再拓扑dp。子树求和之后要先强制赋 s i z [ 1 ] = 0 siz[1]=0 siz[1]=0!
(或许)未完待续qwq