zoukankan      html  css  js  c++  java
  • 强势图解后缀自动机


    题外话:

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

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


    前置知识:字典树

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

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

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

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

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

     基本储存信息:

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

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

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

    后缀自动机的性质:

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

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

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

    ……

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

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

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

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

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

    5、当此时$p$点为$0$时,把$np$的$fail$指针赋为$1$(因为根节点$root$为$1$)

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

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

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

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

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

     

    int last=1,cnt=1;//初始化
    
    void Insert(int c)//插入,联系上文文字看吧
    {
        int p=last,np=++cnt;last=np;len[np]=len[p]+1;
        for(;p&&!ch[p][c];p=fail[p])
            ch[p][c]=np;
        if(!p)
            fail[np]=1;
        else
        {
            int q=ch[p][c];
            if(len[q]==len[p]+1)
                fail[np]=q;
            else
            {
                int nq=++cnt;
                len[nq]=len[p]+1;
                memcpy(ch[nq],ch[q],sizeof(ch[q]));
                fail[nq]=fail[q];
                fail[q]=fail[np]=nq;
                for(;ch[p][c]==q;p=fail[p])
                    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$的某个字串的结尾,把$np$的$fail$指针指向$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$点的问题。但是这种情况与第一种情况的区别在于:第一种情况$p$,$q$中间不会夹杂其他的字符,而这种情况$p$,$q$中间是会有其他字符的,我们就不能保证到达$q$节点的一定是$Tx$的后缀了。

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

    操作完成后就是下图:


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

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

    1、插入$a$字符:

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

    2、插入$c$字符:

     

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

    3、(重点)插入$a$字符

    这种情况就是$len[q]=len[p]+1$的情况了,首先$p$为$3$节点,然而现在$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$

    $p$跳$fail$指针到$root$:

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

    最后我们还要按照下图一样,$p$点跳$fail$指针把所有连向$q$的转移都连向$nq$($nq$代替了$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]=sumlimits_{v: (u,v) in E}f[v]+1$

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

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

    3、字典序第$k$大的子串

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

    4、字典序最小循环移位

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

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

    【模板】循环移位

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

    给定两个字符串为$T$和$S$,求出它们的最长公共子串

    对于这个问题,我们对字符串$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$

  • 相关阅读:
    水晶苍蝇拍:微薄投资感悟摘录(四) (2012-04-03 14:11:01)
    水晶苍蝇拍:投资感悟(三)(手打,有删减)
    水晶苍蝇拍:投资感悟(二)(2011-12-27 08:17:54)
    leetcode -- String to Integer (atoi)
    leetcode -- Longest Palindromic Substring
    leetcode -- Longest Substring Without Repeating Characters
    leetcode -- Add Two Numbers
    QQ截图工具截取
    LUA学习笔记(第5-6章)
    Lua5.2 请求 luasocket 相关模块时的 multiple-lua-vms-detected
  • 原文地址:https://www.cnblogs.com/yexinqwq/p/10076929.html
Copyright © 2011-2022 走看看