后缀自动机初探(模板)+ SPOJ LCS2

版权声明:本文为博主原创文章,转载请著名出处 http://blog.csdn.net/u013534123 https://blog.csdn.net/u013534123/article/details/79620728

        据说后缀自动机这个东西,要先学后缀数组还有后缀树,再学后缀自动机。但是我只学了后缀数组,然后就直接学这个,难免可能有些理解不到位。硬刚了两天吧,才算是有一点小的理解。
       首先呢,最初的源头还是CLJ大佬的的那个ppt,但是说实话,可能因为是本人太渣,有些地方看不懂,或者说证明太多了不想看。论文看了个大致没有深究,也用自己的想法强行理解,可能有些不对,以后再慢慢研究。
        这个东西一开始的模型是仿照AC自动机的,利用字典树建立自动机,这个的结果是这样的:
                                        
      不同于AC自动机,后缀自动机是要把所有的后缀加入自动机,那么按照这种建立方式,空间复杂度最坏是O(N^2),显然不能让我们满意,所以说,我们得寻求一个最小状态后缀自动机。那么如何实现呢?我们回忆一下我们的KMP,里面有一个滑动的过程,即如果到某一个点匹配不到,那么我就往后滑动一个特定的距离使得之前匹配的部分不完全舍弃,而是作为下个匹配串的前缀。这里有类似的方式,即parent树。
        所谓parent树,就是指parent指针组成的树。而parent指针,则类似于AC自动机中的fail指针。不同的地方在于parent指针在继承上起作用。例如,假设要新加入一个字符,那么首先是直接连到上一个字符的后面,然后从上一个字符的parent指针开始一直往上,所有的节点都要连接这个新加入的字符,直到出现一个字符之前已经连过该字符或者到根了。这又是为什么呢?由于这是后缀自动机,新加入一个字符后,所有的后缀都会发生改变,因此parent往上都要更改。举个例来说,字符串ababab,如果还要加入一个x(顺序自右向左),第一个b连接x,从头开始匹配可以表示后缀abababx;第二个b连接x,从头开始匹配可以表示后缀ababx;以此类推等等。这里的parent就是指当前状态最长串的后缀。
        知道了parent指针的意义,那么建立自动机就是按照这个来了。每次动态的加入一个字符,一直往上遍历parent指针,连接新的边。那么新加入的点的parent指针应该指向哪一个点呢?由于其性质,肯定是要找到一个能够表示其后缀的一个状态,显然这个状态的字符得与新加入字符相等。又因为要尽量长,所以是第一个这样的点。但是注意到,aabb,第一个b虽然字符与新加入的第二个b相同,但是aab显然不是aabb的后缀,所以说第一个这种点也不一定满足条件。这里后缀自动机里面为了解决这个问题引入了一个len参数,表示从根开始走到某一状态的最长长度。我们假设上面例子第一个b状态位编号i,它的parent为fa,如果len[fa]+1<len[i],那么可以断定i这个状态肯定不是aabb的后缀。解决方法就是,我新建立一个状态np替代i,然后把i和当前这个字符的parent都设置为np,而fa则有边直接指向np。这个可能有点难理解,可以结合下图:

                                      
      可以看见,状态4加入之后,找其parent,但是由于上面说的len关系,所以说得新建一个状态5,然后从fa直接连接状态5,并且为i和x的parent。上面的图就可以完美的表示所有的后缀,进而也可以表示所有的子串(子串即可以不走到末尾)。下面给出建立后缀自动机的代码:
void ins(int x,int id)
{
	int p=cur; cur=++tot; T[cur].len=id;  //注意这里的cur是全局变量,用途在于加入6的时候首先是4与其连接而不是5
      	for(;p&&!T[p].ch[x];p=T[p].fa) T[p].ch[x]=cur;  //按照parent往上走,知道走到一个连接过的点或者root
        if (!p) {T[cur].fa=1;return;}int q=T[p].ch[x];  //如果走到root,那么parent直接就是root
        if (T[p].len+1==T[q].len) {T[cur].fa=q;return;}   //如果满足len的条件,那么parent直接是q
        int np=++tot; memcpy(T[np].ch,T[q].ch,sizeof(T[q].ch));  //否则就新建立点,np继承q的所有连接,len会增长
        T[np].fa=T[q].fa; T[q].fa=T[cur].fa=np; T[np].len=T[p].len+1;  //np作为p和q的parent
        for(;p&&T[p].ch[x]==q;p=T[p].fa) T[p].ch[x]=np;  //同时修改原本连向q的点,改为连到np
}
       自动机建立完毕了,那么我们就来看看这个玩意儿怎么用。首先,和AC一样,作为自动机本身就是用来匹配的。根据我之前说的,这个里面存了一个字符串所有的子串,因此做一个最长公共子串的问题自然不在话下。对于两个字符串,一个建立后缀自动机另一个在自动机上匹配。从root开始,没匹配成功一个len加一,遇到不匹配的往parent跳,知道找到可以匹配的parent或者走到根。之后len清空为走到状态的len加一,再继续接着匹配。在这个过程中最大的len即为最长公共子串。也很容易理解。

       现在再给出一道例题吧。SPOJ LCS2 。也是一个求最长公共子串的。当然我们现在知道了,LCS除了用dp,还可以用后缀数组以及后缀自动机解决。但这题是多个字符串的LCS,这个设计到的问题就比较多了,因为任意两个字符串的LCS可能与最后所有的LCS并没有任何关系。
  对于后缀自动机,我们首先选择一个字符串建立自动机,然后让其他所有的字符串在上面跑。首先,可以明确,最后的LCS一定是任意字符串的子串,故建立自动机没有错。然后跑的时候,对于每个单独的字符串,我可以算出走到每一个自动机状态点的时候的最长len值。那么对于这个点来说,肯定是每个字符串的len值的最小值(每个串都要满足)。然后最后最小len值最大的点的值即为答案。所以按照这个思路即可,但是还是要注意要parent的转移关系。因为在自动机上走,肯定是先走到parent,再到后面的点。故如果能够走到后面的点,那么parent也一定能够走到,所以对于当前串的这个点,len值可以取到该状态的最大长度。这个可以在后面额外处理,用类似后缀数组的基数排序按照状态最大长度排序,然后再往上继承。具体见代码:
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define N 100010
using namespace std;

int mx[N<<1],mn[N<<1];

struct Suffix_Automation
{
    int tot,cur,c[N<<1],sa[N<<1];
    struct node{int ch[26],len,fa;} T[N<<1];
    void init(){cur=tot=1;memset(T,0,sizeof(T));}

    void ins(int x,int id)
    {
        int p=cur; cur=++tot; T[cur].len=id;
        for(;p&&!T[p].ch[x];p=T[p].fa) T[p].ch[x]=cur;
        if (!p) {T[cur].fa=1;return;}int q=T[p].ch[x];
        if (T[p].len+1==T[q].len) {T[cur].fa=q;return;}
        int np=++tot; memcpy(T[np].ch,T[q].ch,sizeof(T[q].ch));
        T[np].fa=T[q].fa; T[q].fa=T[cur].fa=np; T[np].len=T[p].len+1;
        for(;p&&T[p].ch[x]==q;p=T[p].fa) T[p].ch[x]=np;
    }

    void match(char *s)
    {
        int p=1,len=0;
        memset(mx,0,sizeof(mx));
        for(int i=0;s[i];i++)
        {
            int c=s[i]-'a';
            if (T[p].ch[c]) p=T[p].ch[c],len++;
            else
            {
                for(;p&&!T[p].ch[c];p=T[p].fa);
                if (p==0) p=1,len=0; else len=T[p].len+1,p=T[p].ch[c];
            }
            mx[p]=max(mx[p],len);                        //对于一个字符串,每个状态求一个max
        }
    }

    void getsa()
    {
        for(int i=2;i<=tot;i++) c[T[i].len]++;
        for(int i=1;i<=tot;i++) c[i]+=c[i-1];
        for(int i=tot;i>=1;i--) sa[c[T[i].len]--]=i;
    }

} SAM;

char s[N],t[N];

int main()
{
    int ans=0;
    SAM.init();
    scanf("%s",s);
    memset(mn,INF,sizeof(mn));
    for(int i=0;s[i];i++)
        SAM.ins(s[i]-'a',i+1);
    SAM.getsa();                                        //按照len的长度基数排序,不是用字符串的顺序排序
    while(~scanf("%s",s))
    {
        SAM.match(s);
        for(int i=SAM.tot;i>=1;i--)
        {
            int x=SAM.sa[i];
            int fa=SAM.T[x].fa;
            mn[x]=min(mn[x],mx[x]);                    //对于不同字符串在同一个状态,取一个min
            if (mx[x]&&fa) mx[fa]=SAM.T[fa].len;        //如果儿子能到,那么父亲一定能走最远路到
        }
    }

    for(int i=2;i<=SAM.tot;i++)
        ans=max(ans,mn[i]);
    printf("%d\n",ans);
    return 0;
}

阅读更多
换一批

没有更多推荐了,返回首页