后缀自动机 学习笔记

对于鱼来说,写过模板而不写博客的后果就是过了几个月跟没学过一毛一样。

所以要开始营业了

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 2n1

每个节点包含这样几个基本信息:

  • 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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值