强势图解回文自动机



题外话:

本文为博主原创文章,转载请附上博文链接!https://www.cnblogs.com/yexinqwq/p/10086668.html

其实回文自动机跟其他自动机差不太多吧,(特别是模板代码短qwq

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


 前置知识:

1、马拉车算法(大概吧,其实个人认为不学也没关系qwq

2、Trie


 关于回文自动机:

回文自动机其实就是回文树,是由俄罗斯人MikhailRubinchik2014年夏发明的,然而关于回文自动机,其实它并不是严谨的树形结构,因为它有两棵子树,其中一颗节点编号为0,它的子树是长度为偶数的回文串,并且这个节点长度设为0,而另一棵子树编号为1,它的子树是长度为奇数的回文串特别的是:它的长度设置为1!!!(至于为什么,因为它可以方便代码书写,以后会提到)


 变量声明:

1ch[n][c]:普通的字典树

2fail[n]fail指针,指向当前回文串的最长回文后缀,后面会详细介绍

3len[n]:存放当前节点回文串的长度

4last:最新添加的回文节点

5cnt:总的节点个数


 声明:

对于一个节点,我不称它为当前节点的编号!而是把每个节点当做一个回文串!例如abbaabba这个例子,完整的回文自动机如下(省略fail指针):

 

在上图中,我们对5号节点不叫它5号节点,而叫它abba,(请记住,因为后文这样便于理解),这样叫的原因是0节点经过了b的转移到了4号节点,而就相当于在原来的基础上前后各增加了一个b,即4号节点叫做bb,同样,4号节点经过a的转移到了5号节点,就在前后各加一个a,即abba。但是需要注意的是,因为1号节点len1,而每次长度添加都添加2个,所以这样就可以保证1的子树中的长度都为奇数,这就是1号节点len1的好处,(如果是现在不懂没有关系,请跟着下面的图解一步一步思考!!!

 


大概流程:

在图解之前,还是要让你们朦胧中有一丝印象,知道每步在干吗!(所以这里没看懂的话没关系,到了图解那里一起来理解!),建立回文自动机的过程如下:

初始化:

in[0]='#'可以不是#,只要是不会出现的字符就行。

fail[0]=1cnt=1

1、找到最新建立的节点last,找到以last结尾的最长回文串的开头1的位置,如果和当前位置字符相同的话(假设当前字符为x),说明在last这个回文串的基础上两边都有字符x,也就是说会有一个本质不同的回文串产生,否则跳fail指针,找到以last点结尾的最长回文后缀(不包括自身),最终找到last如果last节点没有x的转移,那么我们就让cnt自增1,即新建了一个节点,len[cnt]=len[last]+2因为我们找到last两边都是x字符,所以新的回文节点长度为原来的+2

2、如果last节点没有x的转移,那么我们就让cnt自增1,即新建了一个节点,len[cnt]=len[last]+2因为我们找到last两边都是x字符,所以新的回文节点长度为原来的+2,同时定义jfail[last],找到以当前节点的最长回文后缀,使fail[cnt]指向它。

3、更新last节点,继续插入下一个字符。

先贴代码,方便以后查看:

 


 

如何构建fail指针以及为什么要这样构建:

回文自动机其实是找回文串的,那么怎么样才能找回文串呢?我们假设现在已插入的字符串为babba,而我们现在要插入

字符b,我们用肉眼可以发现babbab会是一个新的回文串,那么它是怎么构成的呢,我们可以发现它其实是原来的字符串babba的最长回文后缀abba两端各加上b构成的,而为什么要找最长回文后缀呢?

1、为什么要最长:

那么我们思考,假如上面的babba的最长回文后缀我们不当做是abba,而是a,那么它的两端都会是b,也会构成一个回文串bab,但是!如果最长回文后缀是abba的话,两端匹配后,会出现babbab这个回文串,并且!它的最长回文后缀就是bab,也就是说最长会保证每个回文串都在失配的情况被遍历,如果失配了,就继续找当前串的最长回文后缀(不包括自身)

2、为什么要回文:
其实这个问题很简单,如果中间不回文的话,就算两边字符相同,构成的新串也不会是回文的

3、为什么要后缀:

因为新的字符必须得跟以前插入的回文串相联系,如果是babbac,而你要插入b,如果你找的是abba最长的回文但不是后缀,那么其实是匹配不到的,因为它们没有联系。


强势图解开始!!!

首先我们初始化s[0]='#',fail[0]=1cnt=1,也就是下图:

 

1、插入字符a

last一开始为0,而in[ilen[last]1]!=in[i]in[101]!=in[1],也就是说in[0]in[1]不能构成回文串,那么我们就得缩小范围,last就跳fail指针到1节点,此时in[ilen[last]1]==in[i],也就是说in[1]==in[1],也就是自己等于自己,因为我们把回文串的范围由2转为了1,所以现在回文串的长度也就是1,即len[cnt]=len[last]+2也就是1+2,所以这也是1节点长度赋值为1的好处,还保证了1的子树长度为奇数。此时last节点没有a的转移,我们就连一条边向cnt。即下图:

 

现在我们考虑求出cnt节点(2节点)的fail指针,我们要知道fail指针指向当前节点的最长回文后缀(不包括自己),我们可以看到当前节点就是a,没有不包括自己的回文后缀,所以fail指针指向0,可以看着代码模拟一下,最后会是0节点,那么我们再更新last到当前节点,最后如下图:

 

2、插入b字符

last节点为2号节点,那么in[ilen[last]1]!=in[i],即in[0]!=in[2],这个时候我们判断的其实是在a的两边是不是都是b,如果是的话那么肯定是回文串,可是这里并不匹配,所以lastfail指针到0节点,但是我们发现in[ilen[last]1]!=in[i],即in[1]!=in[2],这个时候我们判断的实质是,我们把回文串的范围由刚才的3个变为了2个,现在考虑是不是有两个a在一起构成回文串,可是还是失配了,所以我们再跳fail指针到1节点,而这时其实就是自己匹配自己了,也就是in[2]=in[2],所以len[3]=len[1]+2,也就是1,因为这个回文串就b自身,长度自然为1,我们像上面一样连边:

我们像上面一样求b点的fail指针,因为我们需要找到b的最长回文后缀,但是我们可以直接看出,除了自身之外就没有回文后缀了,所以它的fail指针指向0节点,并且下移last节点到3,自己模拟代码也可以知道,如下图:

 

3、插入字符b

我们看到last节点,而此时我们发现in[ilen[last]1]!=in[i],即in[1]!=in[3],也就是在考虑在3节点(也就是回文串b)的左右是不是都是字符b,如果是的话,那么就找到了更长的回文串,但是现在失配了,所以我们lastfail指针到0节点,此时in[ilen[last]1]=in[i]了,也就是in[2]=in[3],也就是说我们找到了一个长度为2的回文串bb,而0节点没有向b的转移,于是我们就连一条b的转移到新的节点,此节点表示回文串bb,如下图:

 

那么我们考虑求出4号节点的fail指针,我们可以看出除了自身回文串(bb)之外,是它的回文后缀的就是它的最后一个字母b,那么我们就应该指向已有的代表b这个回文串的节点,即3号节点,至于怎么找的后文的例子可以更形象的说明!所以这里不再赘述,最后更新last,操作完后如下:

4、插入a字符:

 我们看着现在的last节点,可以看出in[ilen[last]1]=in[i],即in[1]=in[4],也就是说,我们在last节点的基础上,即回文串bb的两边都找到了字符a,可以构成一个新的长度为4的回文串,而last节点没有a的转移,于是就新建节点,并连边,如下图:

 

【重点】:

那么,我们现在考虑求出5号节点的fail指针,fail指针是要求出当前回文串的不包括自己的最长回文后缀,因为现在last节点在4号节点,我们定义一个新的变量j来跳fail指针,跳到3号节点,不让last改变,(可能会有人问为什么先跳fail再判断呢,为什么不先判断4号节点即in[1]=in[4],而要先跳到3节点去判断呢?那是因为判断4号节点其实就是本身这个回文串,即abba,而我们说fail指针指向的是不包括自己的最长回文后缀),那么我们j跳到3节点之后发现in[ilen[j]1]!=in[i]in[2]!=in[4],也就是串bba不是回文串,我们再跳fail指针到0节点,此时in[ilen[j]1]!=in[i]也就是in[3]!=in[4],因为ba不是回文串,继续跳fail指针,我们到了1节点,判断此时的in[ilen[j]1]=in[i](一定会等于的,因为现在是自己匹配自己,即in[4]=in[4]),说明5号节点的最长回文后缀为a,所以指向代表回文串a的节点,即2号节点,并且更新last节点,如下图:

 

5、插入a字符:

 我们继续像上面一样模拟,看到last节点,我们发现in[ilen[last]1]!=in[i],也就是in[0]!=in[5],我们这个操作实际上是找abba 两端是否相同,如果相同则说明能够成新的回文串,然而失配了。。于是我们继续跳fail指针,到了2号节点,我们发现in[ilen[last]1]!=in[i],即in[3]!=in[5],也就是a的两端相不相等,我们又发现不相等,于是又跳fail指针,到了0节点,我们发现此时in[ilen[last]1]=in[i],也就是in[4]=in[5],说明我们找到了新的回文串aa,而last节点并没有a的转移(也就是以前没找到回文串aa ),我们新加一条边到新的节点,如下图:

 

我们继续向上面一样求fail指针,jfail指针到2号节点,而此时in[ilen[j]1]=in[1]也就是in[5]=in[5],即自己匹配了自己,所以回文串aa的除自身外最长回文后缀就是a,所以fail[5]为表示回文串a的节点,即2号节点,同时下移last,操作完后如下图:

 

(后文的插入就留给你们自己动手模拟了,本人就贴个图了,我觉得应该要给你们自己动手实战一下,其实是本人太懒qwq

6、插入b字符

 

7、插入b字符:

 

8、插入a字符:

 


 

尾声:

本篇文章到此结束,如果觉得有帮助的话,希望能够点个赞qwq

posted @   模拟退火  阅读(1976)  评论(5编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 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】
点击右上角即可分享
微信分享提示