强势图解回文自动机

3493441984zz

2018-12-15 15:05:47

Algo. & Theory

题外话:

更好的阅读体验请点这里

如果本文图片炸了,请到上面链接去看。。。。

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

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

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

前置知识:

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

2、Trie

关于回文自动机:

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

变量声明:

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

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

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

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

5、cnt:总的节点个数

声明:

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

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

大概流程:

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

初始化:

$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,同时定义jfail[last],找到以当前节点的最长回文后缀,使fail[cnt]指向它。

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

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

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]=1cnt=1,也就是下图:

1、插入字符a

![](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