本文只介绍关于
后缀自动机是什么
类似
我们首先要知道,
事实上
在下文中我们将定义
后缀自动机的定义
图中存在一个源点
每个边都标有一些字符,从一个节点出发的所有边都不同(也就是说没有相同的字母作为边)
存在至少一个终止的状态,从
下文中我们定义以下内容
-
endpos " role="presentation">代表一个串的结束位置

这样我们可以把子串根据
endpos " role="presentation"> 分为若干种的等价类我们可以得到几个重要引理(钦定
| u | ≤ " role="presentation">)| w | -
当字符串
u " role="presentation"> 在s " role="presentation"> 中的每次出现,都是以w " role="presentation"> 后缀的形式存在时,字符串s " role="presentation"> 的两个非空子串u " role="presentation"> 和w " role="presentation"> 的endpos " role="presentation"> 相同证明:
-
若
u " role="presentation"> 和w " role="presentation"> 的endpos " role="presentation"> 相同,则u " role="presentation"> 是w " role="presentation"> 的一个只以s " role="presentation"> 中的一个w " role="presentation"> 的后缀的形式出现后缀 -
若
u " role="presentation"> 为w " role="presentation"> 的一个后缀且在s " role="presentation"> 中出现时只以后缀的形式,两个子串的endpos " role="presentation"> 相同。
因此得证
-
-
u " role="presentation"> 和w " role="presentation"> 的endpos " role="presentation"> 集合要么不交,要么完全包含两者关系为
" role="presentation">{ endpos ( w ) ⊆ endpos ( u ) u 是 w 的后缀 endpos ( w ) ∩ endpos ( u ) = ∅ 证明:
-
在
u " role="presentation"> 是w " role="presentation"> 的后缀时,类似上图内的 “能够接收 " role="presentation"> 的串的节点一定可以接受a b " role="presentation">”b -
若集合
endpos ( u ) " role="presentation"> 与endpos ( w ) " role="presentation"> 有至少一个公共元素,那么由于字符串u " role="presentation"> 与w " role="presentation"> 在相同位置结束,u " role="presentation"> 是w " role="presentation"> 的一个后缀。 -
所以在每次
w " role="presentation"> 出现的位置,子串u " role="presentation"> 也会出现。所以endpos ( w ) ⊆ endpos ( u ) " role="presentation">
因此得证
-
-
将一个
endpos " role="presentation"> 等价类中的所有子串按长度非递增的顺序进行排序每个子串长度都小于等于它前一个子串且每个子串是它前一个子串的后缀
因此对于同一等价类的任一两子串,较短者为较长者的后缀且类中的子串长度恰好覆盖整个区间
[ x , y ] " role="presentation"> 。
-
-
len " role="presentation">代表一个节点能控制的最大长度,比如当一个节点可以控制
a " role="presentation"> 和a b " role="presentation"> 时其控制的最大长度(len " role="presentation"> )也就是a b " role="presentation"> 代表的2 " role="presentation"> -
fa " role="presentation">一个节点其连接的
fa " role="presentation"> 节点是其的一个后缀,因为节点是按照endpos " role="presentation"> 来分的,所以其的fa " role="presentation"> 是其后缀内endpos " role="presentation"> 集合不同的最长的后缀
后缀自动机的构建过程
假设要建立的串是

然后加入一些边来维护后缀自动机,让后缀自动机能够记录所有的后缀

再比如说要建立的串是

我们发现,在第一个建立完毕的
因此我们需要引入一个名为
p a r e n t " role="presentation"> 树的建立
后缀链接 l i n k " role="presentation">
对于
定义
考虑
定义
同时,字符串
记最长的这样的后缀为
一个 后缀链接
通过

如上图
其中这些链接这些节点的边其实和后缀链接
我们可以发现最后建立的一定是一颗树的形状,其中根节点一定是
这棵树就是我们的
定理
-
设一个
" role="presentation"> 等价类为e n d p o s " role="presentation"> ,E " role="presentation"> 中最短的串为E s " role="presentation"> 。那么
" role="presentation"> (这里因为如果相等会合并为一个e n d p o s ( v ) ⊊ e n d p o s ( l i n k ( v ) ) " role="presentation"> 所以不能取等)e n d p o s -
l i n k ( E ) " role="presentation"> 中最长的串是s " role="presentation"> 的长度为| s | − 1 " role="presentation"> 的真后缀。
S A M " role="presentation"> 的建造
我们把
下面的是以

构造算法
下面的是
初始化:
-
令
l a s t " role="presentation"> 为添加字符 " role="presentation"> 之前整个字符串c h S " role="presentation"> 所对应的结点(从 " role="presentation"> 出发走到t 0 l a s t " role="presentation"> 的路径上边组成的字符串就是S " role="presentation"> ,初始l a s t = 0 " role="presentation"> )。 -
创建一个新的状态
c u r " role="presentation"> ,让l e n ( c u r ) = l e n ( l a s t ) + 1 " role="presentation"> -
从
l a s t " role="presentation"> 开始对 " role="presentation"> 进行遍历,若当前结点l i n k v " role="presentation"> 没有v → c u r " role="presentation"> 的边,创建一条v → c u r " role="presentation"> 的边并标记为 " role="presentation">c h -
若遍历到了
" role="presentation"> 则让t 0 l i n k ( c u r ) = 0 " role="presentation"> ,l a s t = c u r " role="presentation"> ,回到第一步 -
如果当前结点
v " role="presentation"> 已经有了标记字符 " role="presentation"> 的出边则停止遍历,并把这个结点标记为c h p " role="presentation"> ,标记p " role="presentation"> 沿着标记字符 " role="presentation"> 的出边到达的点为c h q " role="presentation"> 。 -
如果
" role="presentation"> ,让l e n ( p ) + 1 = l e n ( q ) l i n k ( c u r ) = q , l a s t = c u r " role="presentation"> ,回到第一步。 -
复制状态
q " role="presentation"> 到一个新的状态 " role="presentation"> 里(包括E " role="presentation"> 以及所有l i n k ( q ) q " role="presentation"> 在 " role="presentation"> 上的出边),将D A G " role="presentation"> 赋值为l e n ( E ) l e n ( p ) + 1 " role="presentation"> 。复制之后,再赋值
l i n k ( q ) = E , l i n k ( c u r ) = " role="presentation">E 然后从
p " role="presentation"> 遍历后缀链接,设当前遍历到的点为v " role="presentation"> ,若v " role="presentation"> 有标记为 " role="presentation"> 的出边c h v → q " role="presentation"> ,则重定向这条边为v → " role="presentation">E 若
v " role="presentation"> 没有标记为c " role="presentation"> 的出边或者v " role="presentation"> 的标记为c " role="presentation"> 的出边所到达的点不是q " role="presentation"> ,停止遍历,l a s t = c u r " role="presentation"> ,回到第一步
我们一步一步的来看这个算法
-
找到
l a s t " role="presentation"> 来为后面的更新做准备 -
加入
" role="presentation"> 后,我们会形成一个新的等价类(因为c h " role="presentation"> 包含了新字符e n d p o s " role="presentation"> 的位置),我们定义这个新的等价类为c h " role="presentation">c u r 根据对
" role="presentation"> 的定义,我们可以发现l a s t " role="presentation"> 内最长的字符串一定是l a s t S " role="presentation">,所以| S | = " role="presentation">l e n ( l a s t ) 我们知道,新加入的字符是
" role="presentation"> ,所以新串是c h S + " role="presentation">,我们需要链接一条从c h S " role="presentation"> 到c u r " role="presentation"> 的边,标记为 " role="presentation"> 表示可以从c h S " role="presentation"> 转移到S + " role="presentation">c h 那么等价类
c u r " role="presentation"> 内的最长字符串一定是S + " role="presentation">,长度显然为c h l e n ( l a s t ) + 1 " role="presentation"> -
遍历
" role="presentation"> 并尝试加入关于l i n k c u r " role="presentation"> 的转移,我们知道 " role="presentation"> 是记录的所有后缀,我们只要在后面加入一个S A M " role="presentation">c h 我们知道,从
l a s t " role="presentation"> 开始跳 " role="presentation"> 可以跳到所有后缀,只要依次在这些等价类内向l i n k c u r " role="presentation"> 试图加入标记为 " role="presentation"> 的转移即可c h 然而我们只能添加与原有转移不冲突的转移。因此我们只要找到已存在的
" role="presentation"> 的转移,我们就必须停止c h 下面的图片是对于字符串
S = a b c b a " role="presentation"> 加入一个新的字符d " role="presentation"> 后的结果
我们发现当从
" role="presentation"> 一直跳l a s t " role="presentation"> 跳到了l i n k " role="presentation"> 节点,那么证明t 0 " role="presentation"> 内没有出现过S A M " role="presentation"> 节点c h -
我们到达了虚拟状态
− 1 " role="presentation"> ,这意味着我们为所有S " role="presentation"> 的后缀添加了 " role="presentation"> 的转移。这也意味着,字符c h " role="presentation"> 从未在字符串c h S " role="presentation"> 中出现过。因此
c u r " role="presentation"> 的后缀链接为状态0 " role="presentation"> -
第二种情况,我们找到了
( p , q ) " role="presentation"> 的状态转移,说明我们正在试图添加一个已经存在的x + " role="presentation"> (c h x " role="presentation">为当前这个节点代表的S " role="presentation"> 的后缀)我们似乎不应该在这里加入一个标记为
" role="presentation"> 的转移,所以我们应该把c h " role="presentation"> 连到一个l i n k ( c u r ) " role="presentation"> 为l e n " role="presentation"> 的状态上l e n ( p ) + 1 -
如果
l e n ( q ) = l e n ( p ) + 1 " role="presentation">,也就是说转移( p , q ) " role="presentation"> 是连续的我们只需要让
" role="presentation"> 连到l i n k ( c u r ) q " role="presentation"> 即可 -
如果
l e n ( q ) > l e n ( p ) + 1 " role="presentation">,也就是转移( p , q ) " role="presentation"> 并非连续那么这意味着状态
q " role="presentation"> 不只对应于长度为l e n ( p ) + 1 " role="presentation"> 的后缀S + " role="presentation"> ,还对应于c h s " role="presentation"> 的更长的子串我们就被迫只能把
q " role="presentation"> 一分为二了,让第一个状态 " role="presentation"> 的E " role="presentation"> 为l e n l e n ( p ) + 1 " role="presentation">首先我们对于
q " role="presentation"> 复制一个状态 " role="presentation">,让E " role="presentation">,然后把l e n ( E ) = l e n ( p ) + 1 q " role="presentation"> 的所有转移复制到 " role="presentation"> 上E 同时,我们把
" role="presentation"> 的目标也设为l i n k ( E ) " role="presentation"> 的目标,并且让l i n k ( q ) " role="presentation"> 的目标设定为l i n k ( q ) " role="presentation">E 接下来我们重定向一些边(因为分开之后我们必须要区分清哪些入边构成了被分出去的
" role="presentation">,哪些构成了还剩下在E q " role="presentation"> 中的串),然后继续遍历 " role="presentation"> 并且分类讨论l i n k -
找到了一个结点
v " role="presentation"> 拥有一条标记 " role="presentation"> 的出边指向c h q " role="presentation">说明
v " role="presentation"> 中的串加上 " role="presentation"> 构成了c h " role="presentation"> 中的串,我们要把这条边重定向到E " role="presentation"> 。E -
找到了一个结点
v " role="presentation"> 拥有一条标记 " role="presentation"> 的出边,但指向结点不是c h q " role="presentation">说明
v " role="presentation"> 中的串加上 " role="presentation"> 构成了一个不在c h " role="presentation"> 集合中的E " role="presentation"> 内最长串的后缀(E " role="presentation"> 在E " role="presentation"> 树上的某一级祖先中的一个串)。p a r e n t 因为
v " role="presentation"> 中的串加上 " role="presentation"> 无法构成c " role="presentation"> 中的串,则E " role="presentation"> 的后缀加上v " role="presentation"> 显然也无法构成c h " role="presentation"> 中的一个串。E 也就是说,我们已经把所有要重定向的边完成重定向了,停止遍历即可。
-
关于 S A M " role="presentation"> 线性复杂度的证明
在使用 map 的情况下时间复杂度为
在不使用 map 的情况下时间复杂度
警惕 SAM 题目常见卡 unordered_map 的坏人
我们认为字符集的大小为常数,即每次对一个字符搜索转移、添加转移、查找下一个转移的时间复杂度都为
在算法中有三处不是明显线性的,我们一一来分析
-
遍历所有状态
" role="presentation"> 的后缀链接,添加字符l a s t " role="presentation"> 的转移。c h 对于后缀自动机大小,很明显转移数和状态数是线性的,所以这里一定是线性的
-
当状态
q " role="presentation"> 被复制到一个新的状态 " role="presentation"> 时复制转移的过程E 同上一条,后缀自动机的转移数和状态数很明显是线性的,所以是线性的
-
第三处是修改指向
q " role="presentation"> 的转移,将它们重定向到 " role="presentation"> 的过程。E 结论:
l o n g e s t ( l i n k ( l i n k ( l a s t ) ) " role="presentation"> 的位置单调递增,所以这个循环最多不会执行超过n " role="presentation"> 次迭代证明:
-
将最初指向
q " role="presentation"> 的转移重定向到 " role="presentation">,记E v = l o n g e s t ( p ) " role="presentation"> ,这是一个字符串s " role="presentation"> 的后缀,每次迭代长度都递减(字符串s " role="presentation"> 的位置每次迭代都单调上升)。 -
这种情况下,如果在循环的第一次迭代之前,相对应的字符串
v " role="presentation"> 在距离 " role="presentation"> 的深度为l a s t k ( k ≥ 2 ) " role="presentation"> 的位置上(深度记为后缀链接的数量) -
那么在最后一次迭代后,字符串
v + " role="presentation"> 将会成为路径上第二个从c h " role="presentation"> 出发的后缀链接(它会成为新的c u r " role="presentation"> 的值)l a s t -
所以
l o n g e s t ( l i n k ( l i n k ( l a s t ) ) " role="presentation"> 的位置单调递增
-
实现
下面是基于 map 实现的 SAM,复杂度为
当 SAM 跑的不应为字符串即为数字时改变 valueType 即可
点击查看代码
#include
typedef char valueType;
namespace Suffix_AutoMaton{
#define for_(a,b,ch) for(int a=b;a<=ch;a++)
#define _for(a,b,ch) for(int a=b;a>=ch;a--)
constexpr int MaxLen=1e5+5;
class SAM{
public:
int len, Link;
std::map<valueType,int> Next;
}sam[MaxLen<<1];
int Size,Last;
inline void Init() {
sam[0].len=0;
sam[0].Link=-1;
Size++;Last=0;
}
inline void Insert(valueType ch) {
int cur=Size++,p=Last;
sam[cur].len=sam[Last].len+1;
while((p!=-1)&&(
!sam[p].Next.count(ch))){
sam[p].Next[ch]=cur;
p=sam[p].Link;
}
if(p==-1)
sam[cur].Link=0;
else {
int q=sam[p].Next[ch];
if(sam[p].len+1==sam[q].len)
sam[cur].Link=q;
else{
int E=Size++;
sam[E].len=sam[p].len+1;
sam[E].Next=sam[q].Next;
sam[E].Link=sam[q].Link;
while(p!=-1&&sam[p].Next[ch]==q){
sam[p].Next[ch]=E;
p=sam[p].Link;
}
sam[q].Link=sam[cur].Link=E;
}
}
}
#undef for_
#undef _for
}using namespace Suffix_AutoMaton;
signed main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
}
性质
-
对于字符串
S " role="presentation"> ,它的 " role="presentation"> 中的状态数 不会超过S A M 2 | S | − 1 " role="presentation">证明:
-
算法本身就可以证明,首先一开始我们会自带状态,然后第一轮和第二轮最多只会创建
1 " role="presentation"> 个状态后面的每轮最多只会创建
2 " role="presentation"> 个状态,所以不会超过2 | S | − 1 " role="presentation">
-
-
对于字符串
S " role="presentation"> ,它的 " role="presentation"> 中的转移数 不会超过S A M 3 | S | − 4 " role="presentation"> -
分配结束标记
我们知道,
" role="presentation"> 是一个能接收所有后缀的最小的S A M " role="presentation">,因此我们要分配结束标记D F A 先构建完整颗
" role="presentation">,这样找到构建完毕后的S A M " role="presentation">,然后从l a s t " role="presentation"> 往根节点跳,路上的所有节点全部打一个结束标记就行了l a s t
应用
详见【学习笔记】 - 字符串基础 : 后缀自动机(进阶篇)
例题
详见【学习笔记】 - 字符串基础 : 后缀自动机(进阶篇)