后缀自动机

定义

字符串 \(s\) 的 SAM 是一个接受 \(s\) 的所有后缀的最小 DFA(确定性有限(状态)自动机)。也就是:

  • SAM 是一个 DAG。节点为状态,边为转移。

  • 图的源点 \(t_0\) 称初始状态。整张图从 \(t_0\) 开始可以遍历到。

  • 转移标有若干字母,从一个节点出发的所有转移均不同。

  • 存在若干终止状态。从 \(t_0\) 到终止状态形成的转移是 \(s\) 的一个后缀。\(s\) 的每个后缀均能由转移得来。

  • 在所有满足上述条件的自动机中,SAM 的节点数最少。


子串性质

SAM 包含 \(s\) 所有字串的信息。从 \(t_0\) 到任一点形成的转移都是 \(s\) 的子串。\(s\) 的每个子串对应 \(t_0\) 开始的一条路径。

一个状态对应一些字符串的集合,这个集合的每个元素对应这些路径。


构建过程

(建议配合 OI-Wiki 食用)

结束位置 endpos

考虑 \(s\) 的非空子串 \(t\),记 \(\operatorname{endpos}(t)\)\(s\)\(t\) 的所有结束位置。

对于字符串 abcbc\(\operatorname{endpos}(bc)=3,5\).

两个子串 \(t_1\)\(t_2\)\(\operatorname{endpos}\) 可能相等。所有的非空子串可划分为若干等价类。

  • SAM 的每个状态对应若干个 \(\operatorname{endpos}\) 相同的子串。

  • SAM 的状态数等于所有子串的等价类个数及初始状态。

不考虑 SAM 最小性的证明。

一些结论:

  • \(1.\) 字符串 \(s\) 的两个非空子串 \(u\)\(w\)(不妨设 \(|u|\le |w|\))的 \(\operatorname{endpos}\) 相同,当且仅当 \(u\)\(s\) 中的每次出现都以 \(w\) 后缀的形式存在。

  • \(2.\) 上述的 \(u\)\(w\),若 \(u\)\(w\) 的后缀,则 \(\operatorname{endpos}(w)\subseteq\operatorname{endpos}(u)\),否则 \(\operatorname{endpos}(w)\cap\operatorname{endpos}(u)=\varnothing\).

  • \(3.\) 同一等价类的任意两子串,短者为长者的后缀,这一些子串长度覆盖一段完整的区间。


对于非 \(t_0\) 的状态 \(v\),其对应具有相同 \(\operatorname{endpos}\) 的等价类。令 \(w\) 为其中最长者,那么其他的都是 \(w\) 的后缀。

将这些后缀按长度降序排序,那么会有前几个包含于这个等价类,其他后缀(包含 \(\varnothing\))在其他等价类中。令 \(t\) 为后者的最长者,且 \(\operatorname{link}(v)\leftarrow t\).

\(\operatorname{link}(v)\) 对应 \(w\) 的最长后缀的另一个 \(\operatorname{endpos}\) 等价类状态。

\(t_0\) 对应的只包含它自己的等价类为 \(\operatorname{endpos}(t_0)=\{-1,0,\dots,|S|-1\}\).

  • \(4.\) 所有后缀链接构成以 \(t_0\) 为根的树。

\(\operatorname{link}\) 指向的状态对应严格更短的字符串。

  • \(5.\) 通过 \(\operatorname{endpos}\) 集合构造的树(子节点集合 \(\subseteq\) 父节点集合)与通过 \(\operatorname{link}\) 构造的树相同。

构建

SAM 可以在线性时间内构建。

从左到右扫描原串,不断在 SAM 上添加新状态。

维护指针 \(last\) 表示当前字符对应的状态。一开始指向初始状态。

  • 读入字符 \(c\),创建新状态 \(cur\),令 \(\operatorname{len}(cur)\leftarrow\operatorname{last}+1\),表示以 \(c\) 结尾的最长子串比 \(last\) 表示的最长子串多了一个字符。

  • \(last\) 开始沿着后缀链接往上跳,找到状态 \(p\) 满足 \(p\) 有一条字符为 \(c\) 的转移边或 \(p=t_0\)。若 \(p\) 满足前一个条件,记这条边指向的状态为 \(q\),判断 \(\operatorname{len}(p)+1=\operatorname{len}(q)\) 是否成立。

  • 相等则 \(q\)\(cur\) 的后缀链接,令 \(\operatorname{link}(cur)\leftarrow q\),在 \(p\)\(cur\) 之间添加字符为 \(c\) 的转移边。

  • 不相等则 \(q\) 表示多个不同长度的子串,将 \(q\) 复制出一个新状态 \(tp\),令 \(\operatorname{len}(tp)\leftarrow\operatorname{len}(p)+1\),表示 \(tp\) 仅表示长为 \(\operatorname{len}(p)+1\) 的子串。令 \(p\) 指向 \(q\) 的转移重新指向 \(tp\),令 \(\operatorname{link}(q),\operatorname{link}(cur)\leftarrow tp\).

  • \(p\)\(cur\) 之间添加字符为 \(c\) 的转移边。另外地,若 \(p=t_0\),令 \(\operatorname{link}(cur)\leftarrow p\).

  • \(last\leftarrow cur\),继续扫串。

假设字符集大小 \(|\Sigma|\) 为常数,则构建复杂度线性,否则渐进复杂度
\(O(n\log|\Sigma|)\).


代码实现

空间是 \(O(n|\Sigma|)\) 的。

建议 1-index.

无宏定义
struct state{
	int len,link,siz;
	map<int,int>next;
}st[N];
int node,last;
void SAM_init(){
	st[1].len=0,st[1].link=0;
	node=1,last=1;
}
void SAM_extend(int c){
	int cur=++node;
	st[cur].len=st[last].len+1,st[cur].siz=1;
	int p=last;
	while(p&&!st[p].next.count(c)){
		st[p].next[c]=cur;
		p=st[p].link;
	}
	if(!p)st[cur].link=1;
	else{
		int q=st[p].next[c];
		if(st[p].len+1==st[q].len)
			st[cur].link=q;
		else{
			int tp=++node;
			st[tp].len=st[p].len+1;
			st[tp].next=st[q].next;
			st[tp].link=st[q].link;
			while(p&&st[p].next[c]==q){
				st[p].next[c]=tp;
				p=st[p].link;
			}
			st[q].link=st[cur].link=tp;
		}
	}
	last=cur;
}
宏定义
struct state{
	int len,link,siz;
	int nxt[26];
	#define len(x) st[x].len
	#define link(x) st[x].link
	#define siz(x) st[x].siz
	#define nxt(x,c) st[x].nxt[c]
}st[N];
int node,last;
void init(){
	st[1].len=0,st[1].link=0;
	node=last=1;
}
void extend(int c){
	int cur=++node;
	len(cur)=len(last)+1,siz(cur)=1;
	int p=last;
	while(p&&!nxt(p,c))
		nxt(p,c)=cur,p=link(p);
	if(!p)link(cur)=1;
	else{
		int q=nxt(p,c);
		if(len(p)+1==len(q))link(cur)=q;
		else{
			int tp=++node;
			st[tp]=st[q],siz(tp)=0,len(tp)=len(p)+1;
			while(p&&nxt(p,c)==q)
				nxt(p,c)=tp,p=link(p);
			link(q)=link(cur)=tp;
		}
	}
	last=cur;
}
  • 建议全部开 \(2\) 倍空间。

  • SAM 的状态数的上界为 \(2n-1(n\ge 2)\).

  • SAM 的转移数的上界为 \(3n-4(n\ge 3)\).


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

对于 \(s\) 的所有出现次数 \(>1\) 的子串 \(t\),最大化

\[\operatorname{len}(t)\times \operatorname{occur}(t) \]

\(\operatorname{cnt}(v)=|\operatorname{endpos}(v)|\),即对应字符集的出现次数。

感性思考就是 \(\displaystyle\operatorname{cnt}(u)=\sum_{\operatorname{link}(v)=u}\operatorname{cnt}(v)\).

长度即当前节点的 \(\operatorname{len}\) 值。

在 DAG 上做可以先将节点按照 \(\operatorname{len}\) 排序。

\(\operatorname{link}\) 构成的树其实就是所说的 \(\operatorname{parent}\) 树。

0-index

1-index


P4070 [SDOI2016] 生成魔咒

在线维护不同子串总数。

加入一个字符,设当前状态为 \(cur\),则 \(\Delta ans=\operatorname{len}(cur)-\operatorname{len}(\operatorname{link}(cur))\).

record


LCS - Longest Common Substring

求两个字符串 \(S\)\(T\) 的 LCS(最长公共子串)的长度。

\(S\) 构建 SAM。

对于 \(T\) 的每一个前缀,在 \(S\) 中寻找该前缀的最长后缀。

记当前状态为 \(v\),当前长度为 \(l\)。初始状态 \(v=t_0\)\(l=0\).

现在添加字符 \(T_i\)

  • 存在 \(v\)\(T_i\) 的转移,直接转移且令 \(l\leftarrow l+1\).

  • 不存在转移,缩短匹配部分,令 \(v\leftarrow\operatorname{link}(v)\)\(l\leftarrow\operatorname{len}(v)\).

直至存在到 \(T_i\) 的转移,令 \(l\leftarrow l+1\),或者跳到 \(v=t_0\),此时 \(T_i\) 不在 \(S\) 中出现,令 \(v\leftarrow0,l\leftarrow0\).

答案为所有 \(l\)\(\max\).

时间复杂度 \(O(|T|)\),因为实现中要么 \(l\) 加一,要么 \(v\) 沿着 \(\operatorname{link}\) 跳,每次减小 \(l\) 的值。

record


P5341 [TJOI2019] 甲苯先生和大中锋的字符串

一个出现了 \(k\) 次的子串对其长度贡献为 \(1\).

求贡献最大的那个。若有相等的,取长度最大的。

像模板题那样求出 parent 树每个节点的字数大小。

对于 \(\operatorname{siz}(i)=k\)\(i\),其对区间 \((\operatorname{len}(\operatorname{link(i)}),\operatorname{len}(i)]\) 有贡献。

差分即可。

record


Fake News (hard)

对于 \(s\) 的所有子串 \(p\),求 \(\operatorname{occur}(s,p)^2\) 的和。

一个子树对答案的贡献即 \(\operatorname{siz}(v)^2\cdot(\operatorname{len}(v)-\operatorname{len}(\operatorname{link}(v)))\).

record


P3975 [TJOI2015] 弦论

求字符串中的第 \(k\) 小子串。子串可重可不重。

\(k\) 小子串即 SAM 中字典序第 \(k\) 小的路径。

对字符集暴力枚举转移,时间复杂度 \(O(|ans|\cdot|\Sigma|)\),其中 \(ans\) 为最终答案。

子串可重可不重对应了 \(\operatorname{endpos}\) 贡献为它的值本身或者 \(1\)

record


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

  • \(s_1,\dots,s_n\) 的本质不同子串个数。

  • 求能接受 \(s_1,\dots,s_n\) 的所有后缀的最小 DFA 的点数。

广义 SAM 是对 Trie 建 SAM 的过程。插入的时候将当前状态设为其父亲在 SAM 上的编号。

record

Warning
  • 一定要用这种方法建 GSAM。。。不知道自己写的为什么会错。

P6640 [BJOI2020] 封印

多次询问 \(s[l:r]\)\(t\) 的最长公共子串。

对于位置 \(i\) 求出对于 \(s\) 的前缀 \(i\),其是 \(t\) 的子串的最长后缀长度 \(len_i\),可以对 \(t\) 建 SAM,让 \(s\) 在 SAM 上匹配。

询问 \([l,r]\) 的答案为

\[\max_{i=l}^{r}\{\min(len_i,i-l+1)\} \]

  • \(len_i\le i-l+1\),可得 \(l\le i-len_i+1\).

右式单调不降。将分段函数的分界点二分出来。

record


P3346 [ZJOI2015] 诸神眷顾的幻想乡

问树上本质不同子串个数(不包括长为 \(1\) 的串)。叶子数 \(\le 20\).

直接建 SAM 不能处理 lca 非根的情况。

令每个叶子为根建 GSAM 就好了。

record


Paper task

求括号串的本质不同非空合法括号序的个数。

和 生成魔咒 不同的地方在于还要处理从 \(\operatorname{link}\) 往前拼接合法括号串的情况。

(\(-1\))\(1\),用线段树维护后缀和数点。

record


JZPGYZ - Sevenk Love Oimaster

给出若干模板串和查询串,问每个查询串是多少模板串的子串。

对模板串建 GSAM,对于一个查询串,其答案为当前状态的子树内的模板串个数。

在 parent-tree 上线段树合并。做法是在线的。

自己写了个屎玩意调不动了。不太考虑补。


Match & Catch

两个字符串,问最短的各只出现一次的公共子串。

值域 \(5000\).

\(S\)\(T\) 建 SAM,枚举 \(S\) 的左端点,不断往右扫匹配。

可以看下代码。

record


Three strings

给出三个串 \(A,B,C\),对每个 \(L\le \min(len_A,len_B,len_C)\),求出 \(A[a:a+L-1]=B[b:b+L-1]=C[c:c+L-1]\) 的三元组 \((a,b,c)\) 的数量。

直接建 GSAM 没了。

record

posted @ 2023-08-06 20:16  SError  阅读(17)  评论(0编辑  收藏  举报