强势图解后缀自动机

 


题外话:

话说我个人觉得后缀自动机其实也并不难,跟AC自动机都差不多吧(特别是模板代码短

如果有任何错误或着有更好的理解,请联系我!

该文好像有问题,建议批判性的观看,主要是我忘得差不多了,不会改了


前置知识:字典树

其实后缀自动机和AC自动机一样都是字典树,下面以acabb为例子来详解:

如果按照普通的字典树来建造的话,是以下的图:

但是我们看着可以发现有很多很多点都是重复的,浪费了很多空间,而且我们看到每个点大部分只有一个儿子,我们就想到利用公共部分把空间压缩,即把某些重复的边删去,连接到别的子树,从而利用公共部分降低空间复杂度,同时我们还要保证新的做法的正确性,并且降低时间复杂度降到大概O(n),为了解决这个问题,后缀自动机诞生了qwq

下图是后缀自动机成型样子(没显示fail指针):

 后缀自动机是这样的:在后缀自动机中,为了节省空间,某个点有可能成为多个结点的儿子,可以保证在后缀自动机中遍历出来的所有字符串不会重复,而且刚好是原串s的所有子串

 基本储存信息:

1ch[N][26]:基本的字典树;

2fail[N]:指向上一个与当前节点等价的点

3len[N]:表示以当前点为终点的子串的长度

后缀自动机的性质:

1、从任意节点到任意节点结束的路径都是文本串T的子串。

2、后缀自动机是一个根为root有向无环图

3、任何一个从任意节点到达任意节点p的路径是从根节点到p节点最长路径的一个后缀

……

算法流程:(假设要插入的字符为c

1、定义一个变量p=last; //last为上一次的节点;

2、定义一个变量np=++cnt  //cnt为节点编号(也可以理解为时间戳吧,反正差不多就是一种顺序),np为新添加的节点

3、last=nplen[np]=len[p]+1  //更新上次的节点以及新节点的长度

4、循环判断:当p点没有到c的转移的话,ch[p][c]=np;即把p点添加到c的转移为np,并且p点跳fail指针,当p点有到c的转移时停止循环

5、当此时p点为0时,把npfail指针赋为1(因为根节点root1

6、否则的话,意味着此时的p点有到c的转移,我们定义q=ch[p][c]

7、当len[q]==len[p]+1时,把fail[np]赋为p并退出即可(具体后面会详细讲)

8、否则的话,我们定义一个新节点nq=++cnt,复制节点qch数组,fail指针给nqlen[nq]=len[p]+1,并且把npqfail指针赋为nq。循环跳p点的fail指针,每次当ch[p][c]==q时,ch[p][c]赋为nq

(看到这里时是会有点懵逼的,等会结合下文图解及分析一起看)

 先贴这部分代码(方便以后使用):

 

 

  

 


强势图解开始!!!

首先,假设我们已经建立好了文本串T的后缀自动机,现在要在后面插入字符x,使自动机成为字符串Tx的后缀自动机,那么我们先建立一个新节点np,并且找到上一个节点p,循环判断如果p没有x儿子的话,那么就向np连一条x的转移,p点跳fail指针,直到p点到了虚拟节点0或者有向x的转移时,停止循环。如果p点此时是因为有向x的转移而退出的循环,即p!=0。假设此时p点向x的转移为q节点,那么就会有以下两种情况:

 1、len[q]=len[p]+1

我们因为想要压缩空间,那么就必须要共用已有的节点,下面是这种情况的图:

 

那么我们先考虑从p点连一条x的转移到np,但是这样可行吗?我们可以看出如果这样连的话,q点就没有办法到达了,也就是说p>q>np这一字串将会被破坏,那么怎么办呢?我们考虑把q当做是np(因为都是向x的转移),也就是说,把q也当做是T的某个字串的结尾,把npfail指针指向q,从而保证算法的正确性(即性质1

说到这可能大家会有疑问,如果我们不走p节点就到了q节点,那就不能保证到q节点的都是Tx的后缀了。其实len[p]=len[q]+1就已经保证了经过了q就必定经过了p,如果不经过p,那就只能从root节点直接来了,为什么呢,我们运用反证法(转自某大佬):

假设原命题不成立,那么就有两种可能:(补充:现在文本串为Tx,也就是说x是终点)

 

.当前的x字符,之前没有出现过.这样的话,有x字符的子串必然是后缀,与假设矛盾.

 

.当前的x字符,之前已经出现过.这样的话,有x字符而不是后缀的子串必然与之前的某个代表字符x的结点连接,而不是与当前的x点连接,否则后缀自动机的性质3早就被破坏了,故也与假设矛盾。

那么结束后的图是这样的(红色虚边为fail指针,实线黑边为ch数组):

2、len[q]>lenp[p]+1

 这种情况就是下图:

也面临着q点能不能当做np点的问题。但是这种情况与第一种情况的区别在于:第一种情况pq中间不会夹杂其他的字符,而这种情况pq中间是会有其他字符的,我们就不能保证到达q节点的一定是Tx的后缀了。

那么我们考虑能不能把这种情况能不能转化为第一种情况呢?答案是肯定的,我们考虑新建一个节点nq,使得len[nq]=len[p]+1,这样的话我们就转化为了第一种问题,那么我们把节点q所有东西复制给新节点nq的话,就让nq充当第一种问题中的q,那么我们把qnpfail指针赋值为nqnqfail指针为p,同时还必须记住让p节点跳fail指针,把所有连向q节点的边都连向nq(因为nq节点代替了q节点)

操作完成后就是下图:

 


 

图解acabb的后缀自动机过程,建议和代码一起理解!

(没有动图。。。动图太快了不好理解其实是本人不会

1、插入a字符:

 

因为p一开始等于1(根节点),而所以根节点没有向a的转移,因此向2连一条向a的转移,然后跳fail指针,然而fail[1]=0,也就是指向了虚拟节点,所以fail[2]=1(指向根节点)

2、插入c字符:

 

建立新的节点3p节点为上一个节点2p没有向c的转移,因此添加一个向c的转移指向3p点跳fail指针到了1,1节点也没有向c的转移,也添加一个向c的转移到3,跳fail指针到0(虚拟节点),所以np节点的fail指针指向1(根节点)

3、(重点)插入a字符

这种情况就是len[q]=len[p]+1的情况了,首先p3节点,然而现在p节点没有向a的转移,于是向np节点连一条a的转移

 

并且p点跳fail指针到1节点,如下图:

 

而这时候我们发现p节点有a的转移指向2号节点, 并且满足上面所述的第一种情况即len[q]=len[p]+1,所以直接把q节点当做np节点

 

fail[np]赋值为q,即下图:

 

4、插入b字符

p节点没有b的转移,于是p点向np连一条b的转移

 

p点跳fail指针:

我们发现p点没有b的转移,于是p点向np连一条b的转移

 

p点跳fail指针:

 

最后p点到了虚拟指针,所以将fail[np]赋值为根节点1,完成后如下图:

 

5、(重点)插入b字符:

这种情况就是第二种情况:len[q]>len[p]+1

 

首先我们发现p节点没有b的转移,于是添加一个到b的转移为np

 

pfail指针到root

 

我们发现p点有b的转移到q节点,并且满足情况2len[q]>len[p]+1),复制q点的信息到nq点(因为它要代替q节点),即fail[nq]=fail[q],并且nq也像q一样连一条b的转移到np,同时把qnpfail指针赋值为nq

 

最后我们还要按照下图一样,p点跳fail指针把所有连向q的转移都连向nqnq代替了q),如下图:

 

最终acabb的后缀自动机就完成啦!

fail指针去掉就是下图:

 


后缀自动机的应用(借鉴了某大佬):

1、检查字符串p是不是文本串T的子串

给定一个文本串T,求字符串p是不是T的子串

首先,我们对文本串T建立后缀自动机,然后在自动机上直接按照p的每个字符来转移,如果转移失败的话,说明p不是T的子串,这些都是因为后缀自动机满足性质1

 2、不同的子串

给你一个文本串,求一共有多少子串

后缀自动机性质2,因为后缀自动机是一个有向无环图,所以我们可以考虑在上面简单的dp,根据性质2,任何子串都会是后缀自动机上的一段路径(包括长度为0的路径),所以我们令f[i]i节点不同路径的条数,即从i节点开始有多少不同子串,状态转移方程就是:f[u]=v:(u,v)Ef[v]+1

那么我们最后的答案的话就是f[1]1因为要去掉根节点长度为0的串

如果要考虑按字典序输出的话,那么就用一个手写栈来写,每次走字典序小的边,走到一个点就输出当前的栈内元素,递归后要回溯!

3、字典序第k大的子串

其实这个问题是基于上面这个问题的,我们既然已经求出了每个点不同路径的条数,那么我们就可以选择性的走k小路径

4、字典序最小循环移位

给定一个文本串T,每次操作可以把最左边的字符移到最右边,请求出字典序最小的循环移位

这个问题的话其实做多了就知道了,我们以T+T来建立后缀自动机,这样的话后缀自动机就会包含每个循环移位的路径,那么我们直接贪心来找字典序最小就行了

【模板】循环移位

5、求两串中的最长公共子串

给定两个字符串为TS,求出它们的最长公共子串

对于这个问题,我们对字符串T建立后缀自动机,对于S的每个前缀,在自动机里转移状态,定义一个l变量,一个pos变量,分别表示现在匹配的长度,以及现在的位置。我们每匹配成功一次,l自增一,直到没有状态转移的时候,我们就跳fail指针,而此时l就要赋值为len[pos],直到pos指向虚拟节点(也就是失配,此时l=0)或匹配完成,而答案就是l的最大值

 6、出现次数

对于一个给定的文本串 $T$,有多组询问,每组询问给一个模式串 p,回答模式串 p 在字符串 $T$ 中作为子串出现了多少次

我们为文本串T建立后缀自动机,为每个节点定义一个变量size,初始化为1根节点与复制节点nq除外,那么我们对每个节点做如下操作:size[fail[pos]]+=size[pos],含义是当节点pos出现了size[pos]次,那么以它为后缀的点也会出现这么多次。最后查询size[t]t就是模式串的状态,查询不到则为0


 练习:

P3804【模板】后缀自动机

没什么好说的,就是模板

 

P3649[APIO2014]回文串

要用用上面的知识点,灵活运用吧qwq,相信你会举一反三的

 


本篇博客就到这里结束了,如果觉得有帮助不要吝啬你们的赞qwq

 

posted @   模拟退火  阅读(468)  评论(2编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示