后缀自动机小结

学习参考

后缀自动机讲解构造通俗版

后缀自动机讲解+应用通俗版

后缀自动机题型+模板

后缀自动机严谨版(内容较全)

后缀自动机个人心得版

后缀自动机洛谷版模板

后缀自动机大佬讲解PPT

如果你看完这些,你后缀自动机基本上没问题了

如果你想会用的话那只要看前4个就可以了(因为博主自己也只看了前四个的一部分......)

下面是我自己的小结

先是一些后缀自动机的定义

ch[x][s]表示的是节点x(也可以说是状态)字符s的节点编号。返回该结点对应的子串加上某个字符后生成的合法子串在后缀自动机中所对应的位置(其实就和字母树一样),如果该指针不存在,就说明这样的子串是不存在的(即不是s的子串)

fa[x]其实就是parent指针,但是注意这里的fa[x]表示的并不是他的父亲是谁,因为上面也说过了一个节点因为代表了多个含义所以他也有多个父亲。所以parent指针指向的位置表示的是如果当前节点可以接某个后缀那么parent指针指向的节点也一定可以接。即上一个可以接受后缀的节点。

right(s)表示的子串s在原串中所有出现位置的右端点的集合。对于right满足right(s)是right(fa(s))的子集。right的求法,按照parent树中深度从大到小,依次将每个状态的right集合并入他fa状态的right集合。

l[x]表示的是从根节点到该节点的最大距离,对于一个状态s,他代表的串的长度区间就是(len[fa],len[x]].

再引进一些不错的性质:

①从root到任意结点p的每条路径上的字符组成的字符串,都是当前串t的子串.

②因为满足性质一,所以如果当前结点p是可以接收新后缀的结点,那么从root到任意结点p的每条路径上的字符组成的字符串,都是必定是当前串t的后缀.

③如果结点p可以接收新的后缀,那么p的fa指向的结点也可以接收后缀,反过来就不行.

然后是一些构造的时候后面会用到的性质

1、每个状态i的点表示的字符串长度的范围是(len[fa[i]]…len[i]]。(从len[fa[i]]+1…len[i])
2、每个状态i表示的所有字符串的出现次数和right集合都是一样的。
3、由fa构成的数叫做parent树,parent树上子节点的right是父节点的子集。
4、后缀自动机的parent树是原串的反向前缀树,那么也是原串的反串的后缀树。
5、两个串的最长公共后缀是在后缀自动机上对应的状态在parent树上的lca的状态。 

我主要说一下的是后缀自动机插入的时候len[q]==len[p]+1和len[q]>len[p]+1的情况

增量法。我们对于每个状态s,记他代表的最长子串的长度为l(就是上面提到的l[x])

考虑我们当前已经有了前}s|-1个字符的后缀自动机,且现在的自动机中[1,|s|-1]处于end状态(就是终结状态)

现在加入第|s|个字符(设为c),我们令新建了的状态为np,显然l[np]=|s|

然后考虑如何转移:我们加入的是一个last->np的转移,我们也应该加入一个fa(last)->np的转移,直到我们发现到达某个状态已经有一个字符c的转移为止。不妨设这个状态是p,设他经过字符c的转移后的状态为q

如果最终到达root,那么直接将fa[np]=root

如果不为根节点root,就分以下两种情况讨论。

1.len[q]=len[p]+1.这就是说明p,q直接相连中间不夹杂其他的字符,那么q代表的所有串的right集合相同,那我们我们将fa[np]=q即刻。为什么要这么做呢?这样是可以节省空间和状态的,这么连接之后q点就代表了他之前的状态以及现在新加入的状态。

len[q]=len[p]+1直接加点解释:这样构造np就不需要代表所有1到np位置的后缀了,只需要代表(len[fa(np)],len[np] ]长度的后缀

 

2.len[q]>len[p]+1.这种情况下q代表的串总,长度不超过len[p]+1的串的right集合会多出一个值{s|,而长度超过它的串这不会,之所以会出现len[q]>len[p]+1的情况就是因为p,q之间可能还存在其他的字符,而我们先加入的状态是不满足的。那么为了维护一个状态中所有串的right相同这一性质,我们需要新建了一个状态nq,nq代表的是原来q代表的串中所有长度不超过len[p]+1的串,因此len[nq]=len[p]+1,nq的其他属性(fa和转移)和原来的q点一致,同时建立新的fa指针fa[q]=fa[np]=nq,也就是用nq代表了q,np两个状态。

然后我们再次从p开始:本来p的c字符转移到的点是q,现在它转移到nq。同理fa[q]的c字符也要转移到nq,直到当前点的c字符转移到的不是q为止。

从w->q表示的是长度小于len[w]+1的后缀 

len[q]>len[p]+1克隆解释:对于从p->q这条路得到的后缀的right(即长度小于len[p]+1的后缀)都需要加上|s|这个位置,而大于len[p]+1的不需要,这样right就不统一了,所以就需要分裂出来

fa[q]=nq解释:因为q现在表示(len[p]+1,len[q]]的后缀,nq表示(len[原来fa[q]],len[p]+1]的后缀

 

 

后缀自动机构造理解的话,更重要的是他的各种应用,他的用法有很多,并且都比较巧妙,所以还是需要取看一下他各种应用的模板题

最后贴一下代码

这个是求原串S中出现次数在[A,B]之间的子串的个数

#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned int uint;
const int N = 1e5+10;

struct Node {
    int f, len, ch[26];
    void init() {
        len = 0, f = -1;
        memset(ch, 0xff, sizeof (ch));
    }
};

ll sz[N<<1];  //sz[i]表示状态i的right(i)的大小,即状态i表示的字符串出现的次数

struct SAM {
    Node e[N<<1];
    int idx, last;
    void init() {
        idx = 0;
        last = newnd();
    }
    int newnd() {
        e[idx].init();
        return idx++;
    }
    void add(int c) {
        int end = newnd();
        int p = last;
        e[end].len = e[p].len + 1;
        for (; p != -1 && e[p].ch[c] == -1; p = e[p].f) {
            e[p].ch[c] = end;
        }
        if (p == -1) e[end].f = 0;
        else {
            int nxt = e[p].ch[c];
            if (e[p].len + 1 == e[nxt].len) e[end].f = nxt;
            else {
                int nd = newnd();
                e[nd] = e[nxt];
                e[nd].len = e[p].len + 1;
                e[nxt].f = e[end].f = nd;
                for (; p != -1 && e[p].ch[c] == nxt; p = e[p].f) {
                    e[p].ch[c] = nd;
                }
            }
        }
        sz[end]=1;
        last = end;
    }
};
SAM sam;
char str[N];
int ws[N];
int tp[N<<1];
ll d[N<<1]; //d[i]表示从状态i出发,不同的子串的数目,即不同的路径数


int main()
{

    int A,B;
    while(scanf("%s%d%d",str,&A,&B)!=EOF)
    {
        sam.init();
        int len=strlen(str);
        for(int i=0;str[i];i++) sam.add(str[i]-'A');  //插入
        //对sam的节点按照len,从小到大排序重新标号,即给定节点的拓扑序
        for(int i=0;i<=len;i++) ws[i]=0;
        for(int i=1;i<sam.idx;i++) ws[sam.e[i].len]++;
        for(int i=1;i<=len;i++) ws[i]+=ws[i-1];
        for(int i=sam.idx-1;i>0;i--) tp[ws[sam.e[i].len]--]=i;

        //按照拓扑序进行遍历
        //ll res=0;
        for(int i=sam.idx-1;i;i--)
        {
            int v=tp[i];
            sz[sam.e[v].f]+=sz[v];
            if(sz[v]>=A&&sz[v]<=B) d[v]++;
            //if(sz[v]>=A&&sz[v]<=B) res+=sam.e[v].len-sam.e[sam.e[v].f].len;
            //我看其他也有直接将len相减来求的,但我不知道为什么这个求出来的串不会重复,但是这个方法书是可以A的,直接输出res就可以了
            for(int j=0;j<26;j++)
                if(sam.e[v].ch[j]!=-1) d[v]+=d[sam.e[v].ch[j]];
        }
        ll ans=0;
        for(int i=0;i<26;i++)
        {
            if(sam.e[0].ch[i]!=-1) ans+=d[sam.e[0].ch[i]];
        }
        for(int i=0;i<sam.idx;i++) sz[i]=0,d[i]=0;

        printf("%lld\n",ans);


        getchar();
    }
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值