首页 > 其他分享 >后缀自动机

后缀自动机

时间:2023-08-06 20:24:40浏览次数:43  
标签:子串 cur 后缀 len st link 自动机 operatorname

定义

字符串 \(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|)\) 的。

struct state{
	int len,link,siz;
	map<char,int>next;
}st[N];
int node,last;
void SAM_init(){
	st[0].len=0,st[0].link=-1;
	node++,last=0;
}
void SAM_extend(char c){
	int cur=node++;
	st[cur].len=st[last].len+1,st[cur].siz=1;
	int p=last;
	while(p!=-1&&!st[p].next.count(c)){
		st[p].next[c]=cur;
		p=st[p].link;
	}
	if(p==-1)st[cur].link=0;
	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!=-1&&st[p].next[c]==q){
				st[p].next[c]=tp;
				p=st[p].link;
			}
			st[q].link=st[cur].link=tp;
		}
	}
	last=cur;
}
  • 建议全部开 \(2\) 倍空间。

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

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


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

对于 \(s\) 的所有子串 \(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}\) 树。

record


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


标签:子串,cur,后缀,len,st,link,自动机,operatorname
From: https://www.cnblogs.com/SError0819/p/17609924.html

相关文章

  • AC自动机
    AC自动机学习笔记AC自动机简介自动机的一种,著名的多模匹配算法可以理解为Trie+KMP结构建立在字典树的基础上先把所有要匹配的模式串全部塞到一个字典树上面然后在上面添加一种指针类似于KMP中的nxt[]数组,AC自动机中的每个节点有一个叫做fail的指针(失配指针)与KM......
  • 广义后缀自动机略记
    终于学\(\text{GSAM}\)了,这是一个非常有意思且精美的结构!对于一颗\(\text{Trie}\)树\(T\),我们可以跟处理普通字符串一样定义出它的“前缀”(根到某点的字符串),“后缀”(某点到叶子的字符串),“子串”(一条直链对应的字符串)。而它的后缀自动机被定义为接受它所有后缀的最小\(\text......
  • WebService如何去掉后缀访问
    创建全局应用程序类Global.asax,在方法Application_BeginRequest并添加如下代码:利用替换的方式实现效果stringpath=Request.Url.ToString();path=Request.Url.LocalPath.ToString();if(path=="/IFS"){Contex......
  • 后缀数组(SA)做题记录
    SA真的是个好东西,好呀好东西。基础定义:$sa$数组:后缀排序后排名为$i$的后缀的起始位置下标。$rk$数组:起始下标为$i$的后缀的排名。$height$数组:后缀排序后排名为$i$和$i-1$的最长公共前缀长度(Lcp)模板:(小bug:在SA代码charch[N];structSuffix_Array{llm=......
  • 后缀自动机的应用
    后缀自动机的原理就不在赘述了,这里主要介绍它的应用。板子:structnode{ intc[26],len,fa;}a[maxn];voidbuild(intx){ intp=las;intnp=las=++tot; a[np].len=a[p].len+1; for(;p&&!a[p].c[x];p=a[p].fa) a[p].c[x]=np; if(!p) a[np].fa=1; else{ intq=a[p].......
  • 后缀排序
    后缀排序本文做复习用,不宜初学用。定义\(sa\)表示排名为\(i\)的位置。\(rk\)表示位置为\(i\)的排名。\(y\)表示按照第二关键字排序排名为\(i\)的位置。\(height\)表示排名为\(i\)和\(i-1\)的后缀的最大前缀\(h\)表示位置为\(i\)和它排名前一位的后缀......
  • 算法学习笔记(27): 后缀排序
    后缀排序本文做复习用,不宜初学用。开篇膜拜Pecco:算法学习笔记(84):后缀数组-知乎(zhihu.com)有些时候,其实\(O(n\log^2n)\)的排序也挺好。又短又简单。其中\(rk[i]\)表示从第\(i\)个字符开始的后缀的排名,\(sa[i]\)表示排名为\(i\)的后缀开始的位置。#includ......
  • npm常用后缀
    –--save、-S参数意思是把模块的版本信息保存到dependencies(生产环境依赖)中,即你的package.json文件的dependencies字段中;–--save-dev、-D参数意思是把模块版本信息保存到devDependencies(开发环境依赖)中,即你的package.json文件的devDependencies字段中;–--save-optional......
  • android mount文件后缀
    实现AndroidMount文件后缀的步骤作为一名经验丰富的开发者,我将教会你如何实现AndroidMount文件后缀的功能。下面是实现这一功能的步骤和具体代码解释。步骤一:配置AndroidManifest.xml文件在AndroidManifest.xml文件中添加以下权限和文件类型声明:<uses-permissionandroid:nam......
  • 为什么文件后缀改了.java显示还是文本文件
    为什么文件后缀改了.java显示还是文本文件在计算机中,文件后缀用于标识文件的类型。根据文件后缀,操作系统会使用相应的程序来打开、编辑或执行文件。例如,文件后缀为".txt"的文件会被认为是文本文件,并使用文本编辑器打开。而文件后缀为".java"的文件则会被认为是Java源代码文件,并使......