强势图解回文自动机

3493441984zz

2018-12-15 15:05:47

Personal

**** # 题外话: [更好的阅读体验请点这里](https://www.cnblogs.com/yexinqwq/p/10086668.html) **如果本文图片炸了,请到上面链接去看。。。。** 本文为博主原创文章,转载请附上博文链接!https://www.cnblogs.com/yexinqwq/p/10086668.html 其实回文自动机跟其他自动机差不太多吧,(特别是模板代码短$qwq$) 如果有任何错误或着有更好的理解,请联系我! **** ## 前置知识: 1、马拉车算法(大概吧,其实个人认为不学也没关系$qwq$) 2、$Trie$ **** ## 关于回文自动机: 回文自动机其实就是回文树,是由俄罗斯人$MikhailRubinchik$于$2014$年夏发明的,然而关于回文自动机,其实它并不是严谨的树形结构,因为它有两棵子树,其中一颗节点编号为$0$,它的子树是长度为偶数的回文串,并且这个节点长度设为$0$,而另一棵子树编号为$1$,它的子树是长度为奇数的回文串,特别的是:它的长度设置为$-1$!!!(至于为什么,因为它可以方便代码书写,以后会提到) **** ## 变量声明: 1、$ch[n][c]$:普通的字典树 2、$fail[n]$:$fail$指针,指向当前回文串的最长回文后缀,后面会详细介绍 3、$len[n]$:存放当前节点回文串的长度 4、$last$:最新添加的回文节点 5、$cnt$:总的节点个数 **** ## 声明: 对于一个节点,我不称它为当前节点的编号!而是把每个节点当做一个回文串!例如abbaabba这个例子,完整的回文自动机如下(省略$fail$指针): ![](https://s1.ax1x.com/2018/12/15/FaVfaT.png) 在上图中,我们对$5$号节点不叫它$5$号节点,而叫它abba,(请记住,因为后文这样便于理解),这样叫的原因是$0$节点经过了$b$的转移到了$4$号节点,而就相当于在原来的基础上前后各增加了一个$b$,即$4$号节点叫做bb,同样,$4$号节点经过$a$的转移到了$5$号节点,就在前后各加一个$a$,即abba。但是需要注意的是,因为$1$号节点$len$为$-1$,而每次长度添加都添加$2$个,所以这样就可以保证$1$的子树中的长度都为奇数,这就是$1$号节点$len$为$-1$的好处,(如果是现在不懂没有关系,请跟着下面的图解一步一步思考!!!) **** ## 大概流程: 在图解之前,还是要让你们朦胧中有一丝印象,知道每步在干吗!(所以这里没看懂的话没关系,到了图解那里一起来理解!),建立回文自动机的过程如下: 初始化: $in[0]=$'#'可以不是#,只要是不会出现的字符就行。 $fail[0]=1$,$cnt=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$,同时定义$j$为$fail[last]$,找到以当前节点的最长回文后缀,使$fail[cnt]$指向它。 3、更新$last$节点,继续插入下一个字符。 ### 先贴代码,方便以后查看: ```cpp scanf("%d",&n);   scanf("%s",in+1);   in[0]='#'; fail[0]=1;len[1]=-1;//初始化 for(int i=1;i<=n;i++) { int j; while(in[i-len[last]-1]!=in[i]) last=fail[last];//匹配(后面会详细解说) if(!ch[last][in[i]-'a'])//新建节点 { len[++cnt]=len[last]+2;//长度比原来多2 j=fail[last]; while(in[i-len[j]-1]!=in[i]) j=fail[j]; fail[cnt]=ch[j][in[i]-'a']; ch[last][in[i]-'a']=cnt; } last=ch[last][in[i]-'a']; } printf("%d",ans); ``` **** ## 如何构建$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]=1$,$cnt=1$,也就是下图: ![](https://s1.ax1x.com/2018/12/15/FaV5iF.png) ![](https://s1.ax1x.com/2018/12/15/FaVoRJ.png) 1、插入字符$a$ $last$一开始为$0$,而$in[i-len[last]-1]!=in[i]$即$in[1-0-1]!=in[1]$,也就是说$in[0]$与$in[1]$不能构成回文串,那么我们就得缩小范围,$last$就跳$fail$指针到$1$节点,此时$in[i-len[last]-1]==in[i]$,也就是说$in[1]==in[1]$,也就是自己等于自己,因为我们把回文串的范围由$2$转为了$1$,所以现在回文串的长度也就是$1$,即$len[cnt]=len[last]+2$也就是$-1+2$,所以这也是$1$节点长度赋值为$-1$的好处,还保证了$1$的子树长度为奇数。此时$last$节点没有$a$的转移,我们就连一条边向$cnt$。即下图: ![](https://s1.ax1x.com/2018/12/15/FaZpzd.png) ![](https://s1.ax1x.com/2018/12/15/FaZCQA.png) 现在我们考虑求出$cnt$节点($2$节点)的$fail$指针,我们要知道$fail$指针指向当前节点的最长回文后缀(不包括自己),我们可以看到当前节点就是$a$,没有不包括自己的回文后缀,所以$fail$指针指向$0$,可以看着代码模拟一下,最后会是$0$节点,那么我们再更新$last$到当前节点,最后如下图: ![](https://s1.ax1x.com/2018/12/15/FaZkeP.png) ![](https://s1.ax1x.com/2018/12/15/FaZEo8.png) 2、插入$b$字符 $last$节点为$2$号节点,那么$in[i-len[last]-1]!=in[i]$,即$in[0]!=in[2]$,这个时候我们判断的其实是在$a$的两边是不是都是$b$,如果是的话那么肯定是回文串,可是这里并不匹配,所以$last$跳$fail$指针到$0$节点,但是我们发现$in[i-len[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$,我们像上面一样连边: ![](https://s1.ax1x.com/2018/12/15/FaZeJg.png) ![](https://s1.ax1x.com/2018/12/15/FaZJFU.png) 我们像上面一样求$b$点的$fail$指针,因为我们需要找到$b$的最长回文后缀,但是我们可以直接看出,除了自身之外就没有回文后缀了,所以它的$fail$指针指向$0$节点,并且下移$last$节点到$3$,自己模拟代码也可以知道,如下图: ![](https://s1.ax1x.com/2018/12/15/FaZtW4.png) ![](https://s1.ax1x.com/2018/12/15/FaZal9.png) 3、插入字符$b$: 我们看到$last$节点,而此时我们发现$in[i-len[last]-1]!=in[i]$,即$in[1]!=in[3]$,也就是在考虑在$3$节点(也就是回文串$b$)的左右是不是都是字符$b$,如果是的话,那么就找到了更长的回文串,但是现在失配了,所以我们$last$跳$fail$指针到$0$节点,此时$in[i-len[last]-1]=in[i]$了,也就是$in[2]=in[3]$,也就是说我们找到了一个长度为$2$的回文串$bb$,而$0$节点没有向$b$的转移,于是我们就连一条$b$的转移到新的节点,此节点表示回文串$bb$,如下图: ![](https://s1.ax1x.com/2018/12/15/FaZwO1.png) ![](https://s1.ax1x.com/2018/12/15/FaZyFO.png) 那么我们考虑求出$4$号节点的$fail$指针,我们可以看出除了自身回文串($bb$)之外,是它的回文后缀的就是它的最后一个字母$b$,那么我们就应该指向已有的代表$b$这个回文串的节点,即$3$号节点,至于怎么找的后文的例子可以更形象的说明!所以这里不再赘述,最后更新$last$,操作完后如下: ![](https://s1.ax1x.com/2018/12/15/FaZ7tS.png) ![](https://s1.ax1x.com/2018/12/15/FaZqpQ.png) 4、插入$a$字符: 我们看着现在的$last$节点,可以看出$in[i-len[last]-1]=in[i]$,即$in[1]=in[4]$,也就是说,我们在$last$节点的基础上,即回文串$bb$的两边都找到了字符$a$,可以构成一个新的长度为$4$的回文串,而$last$节点没有$a$的转移,于是就新建节点,并连边,如下图: ![](https://s1.ax1x.com/2018/12/15/FaepkT.png) ![](https://s1.ax1x.com/2018/12/15/Fae9tU.png) **【重点】**: 那么,我们现在考虑求出$5$号节点的$fail$指针,$fail$指针是要求出当前回文串的不包括自己的最长回文后缀,因为现在$last$节点在$4$号节点,我们定义一个新的变量$j$来跳$fail$指针,跳到$3$号节点,不让$last$改变,(可能会有人问为什么先跳$fail$再判断呢,为什么不先判断$4$号节点即$in[1]=in[4]$,而要先跳到$3$节点去判断呢?那是因为判断$4$号节点其实就是本身这个回文串,即$abba$,而我们说$fail$指针指向的是不包括自己的最长回文后缀),那么我们$j$跳到$3$节点之后发现$in[i-len[j]-1]!=in[i]$即$in[2]!=in[4]$,也就是串$bba$不是回文串,我们再跳$fail$指针到$0$节点,此时$in[i-len[j]-1]!=in[i]$也就是$in[3]!=in[4]$,因为$ba$不是回文串,继续跳$fail$指针,我们到了$1$节点,判断此时的$in[i-len[j]-1]=in[i]$(一定会等于的,因为现在是自己匹配自己,即$in[4]=in[4]$),说明$5$号节点的最长回文后缀为$a$,所以指向代表回文串$a$的节点,即$2$号节点,并且更新$last$节点,如下图: ![](https://s1.ax1x.com/2018/12/15/FaeF1J.png) ![](https://s1.ax1x.com/2018/12/15/Faekc9.png) 5、插入$a$字符: 我们继续像上面一样模拟,看到$last$节点,我们发现$in[i-len[last]-1]!=in[i]$,也就是$in[0]!=in[5]$,我们这个操作实际上是找$abba$ 两端是否相同,如果相同则说明能够成新的回文串,然而失配了。。于是我们继续跳$fail$指针,到了$2$号节点,我们发现$in[i-len[last]-1]!=in[i]$,即$in[3]!=in[5]$,也就是$a$的两端相不相等,我们又发现不相等,于是又跳$fail$指针,到了$0$节点,我们发现此时$in[i-len[last]-1]=in[i]$,也就是$in[4]=in[5]$,说明我们找到了新的回文串$aa$,而$last$节点并没有$a$的转移(也就是以前没找到回文串$aa$ ),我们新加一条边到新的节点,如下图: ![](https://s1.ax1x.com/2018/12/15/Fae3jA.png) ![](https://s1.ax1x.com/2018/12/15/FaeJBt.png) 我们继续向上面一样求$fail$指针,$j$跳$fail$指针到$2$号节点,而此时$in[i-len[j]-1]=in[1]$也就是$in[5]=in[5]$,即自己匹配了自己,所以回文串$aa$的除自身外最长回文后缀就是$a$,所以$fail[5]$为表示回文串$a$的节点,即$2$号节点,同时下移$last$,操作完后如下图: ![](https://s1.ax1x.com/2018/12/15/FaeUN8.png) ![](https://s1.ax1x.com/2018/12/15/Faew9g.png) (后文的插入就留给你们自己动手模拟了,本人就贴个图了,我觉得应该要给你们自己动手实战一下,其实是本人太懒$qwq$) 6、插入$b$字符 ![](https://s1.ax1x.com/2018/12/15/FaeBcj.png) ![](https://s1.ax1x.com/2018/12/15/FaeqUK.png) 7、插入$b$字符 ![](https://s1.ax1x.com/2018/12/15/FaeyBq.png) ![](https://s1.ax1x.com/2018/12/15/FaeR4U.png) 8、插入$a$字符: ![](https://s1.ax1x.com/2018/12/15/FaefCF.png) ![](https://s1.ax1x.com/2018/12/15/Fae4gJ.png) **** # 尾声: 本篇文章到此结束,如果觉得有帮助的话,希望能够点个赞$qwq$