后缀自动机(SAM) 速成抢救blog 洛谷P3804

后缀自动机(SAM) 速成抢救blog 洛谷P3804

在网上看到了一篇很好的关于后缀自动机的博客,他没有说太多的后缀自动机的原理,全篇只有一个要点——如何构建及操作后缀自动机,这是非常好的东西!

所以,先附上这篇大佬的blog:https://www.cnblogs.com/sclbgw7/p/10197629.html

本篇基本借助于这位大佬的理解,并加上自己的一些理解,酌情阅读

后缀自动机(SAM)是一种极其有用的处理字符串的数据结构,可以用于处理几乎任何有关于子串的问题,但以学起来异常困难著称!但是!!!当你学了SAM并熟练地刷了几道题后,你会发现——你之前为了学SAM而强行理解的许多定理,对你应用SAM一点用处也没有!为了引出构造算法,几乎所有博客都会详细地解释“你为啥要这样做”,然鹅。。。(并没有什么卵用)所以这篇blog,是一片速成SAM构建和操作的blog,如果怕你想完全理解SAM,请上百度搜索 “后缀自动机 陈立杰”,保管有用!!(虽然我自己看不懂)

要想运用,首先得构造,在构造的同时,你得明白你定义的数组的含义,这样你才会明白你构造的SAM会得到什么东西,以及他们是如何得到的(粗糙理解即可)。

数组理解:

SAM是一个DAG(有向无环图),每个点代表一个**“状态”,边代表状态转移,边上有一个字母。SAM有一个起始状态(称为起点),从起点开始,沿着边不断走下去,就可以得到一个字符串。记当前停留节点为x,从x走出来的字符串为S,称节点x可代表字符串S。记x可代表的串中 长度最长的串的长度为*le**n*(x)。

另外,除起点外的每个节点还拥有一个**“后缀链接“**,记作fa[x]。后缀链接组成了一棵树,它的其他性质在构建完之后再讲。

存储SAM是利用的类似于Trie树的存储结构,即使用ch[n] [26]数组保存状态转移的边。

构建:

​ 准备工作:建立数组ch,fa,len,准备指针last,cnt。SAM的构造方法是不断地向已经建好的SAM中加入新的节点。last表示上一个被插入的节点,cnt表示SAM中的节点数量。一开始,last=cnt=1,表示只有一个起点的初始SAM。

接下来,每次往SAM里加入一个字符x

  1. 新建节点np=++cnt ,新建节点p ,p=last ,last=np 。
  2. 如果不存在ch[p] [x] ,令ch[p] [x]=np,p=fa[p] 。重复此步骤(while循环)。
  3. 如果到最后还没有一个p拥有儿子x ,令fa[np]=1 ,并退出过程。
  4. 当ch[p] [x] 出现时,令q=ch[p] [x] 。如果len[q]==len[p]+1 ,令fa[np]=q 。退出过程。
  5. 否则,新建节点nq=++cnt ,将q 的儿子都复制给nq ,令len[nq]=len[p]+1 。
  6. 令fa[nq]=fa[q],fa[q]=fa[np]=nq 。
  7. 从p开始沿着后缀链接,将所有ch[p] [x]==q的节点的ch[p] [x] 都替换成 nq。

将你的字符串的所有字符都一一进行如上操作后,你就得到了用你的字符串构建出来的SAM。

void insert(int x)
{
    int np=++cnt,p=last;
    len[np]=len[p]+1,last=np;
    while(p&&!ch[p][x])ch[p][x]=np,p=fa[p];
    if(!p)fa[np]=1;
    else
    {
        int q=ch[p][x];
        if(len[q]==len[p]+1)fa[np]=q;
        else
        {
            int nq=++cnt;len[nq]=len[p]+1;
            memmove(ch[nq],ch[q],sizeof(ch[nq]));
            fa[nq]=fa[q],fa[np]=fa[q]=nq;
            while(ch[p][x]==q)ch[p][x]=nq,p=fa[p];
        }
    }
}

你不需要知道为什么这么操作可以得到SAM,你只需要记下以下的代码,做几道题强化记忆,然后就可以用SAM的性质来秒题了。

后缀自动机的一些性质:

​ 首先,SAM的点数与边数都是O(n)O(n) 的。记住,由于每次插入最多新建两个点,所以应该开字符总量两倍的空间。计算空间时别忘了你开了26倍的c**hch 数组。

在SAM上从起点开始沿着边随便走走,得到的一定是子串。同时,每一个子串都可以在SAM上走出一条唯一对应的路径。也就是说,子串和SAM上从起点开始的每一条路径一一对应。路径数等于子串数。

起点可以看做是代表空串的点。

重点:定义子串的 right集合:这个子串在原串中所有出现的位置的右端点的集合。

比如说:AAAABBAAAAABAAABBAA

子串AAB出现了3次,右端点集合为{5,12,16} 。这就是子串AAB的 right 集合。

一个节点能够代表的所有子串的right集合是一样的。right集合相等的子串一定被同一个节点代表。(所以,我们会使用“节点的right 集合”这个说法。)两个节点的right集合之间要么真包含,要么没有交集。若节点 y 的right 集合包含了节点x的right 集合,那么y能代表的子串均为x能代表的子串的真后缀。

重点:定义节点x的后缀链接fa(x) :如果有一些节点的right集合包含了x的right集合,fa(x)是right集合中大小最小的那一个。

后缀链接们组成了一棵“后缀链接树”(不是后缀树)。后缀链接树的根为起点。若节点y的right集合包含了节点x的right集合,那么y在后缀链接树上是x的祖先。

一个节点的right集合等于他在后缀链接树上的所有儿子的right集合的并集。而且儿子的right集合之间两两没有交集。

每个节点能代表的子串的长度范围是一段连续的区间。这很好理解,因为它们的结束位置都是相同的。

我们求出每个节点能代表的最长串的长度(即len(x))了,那最短长度呢?其实就等于后缀父亲节点的len+1 。也就是说,所有本质不同的子串的数量等于∑len(x)−len(f**a(x)) 。

下面来上一道题 洛谷P3804 后缀数组(模板题)

#include 
using namespace std;
#define ll long long
const int maxn=2e6+7;
char str[maxn];
int l;
int ch[maxn][26],fa[maxn],len[maxn],last=1,cnt=1;//表示上一个被插入的节点,cnt表示SAM中节点的数量,初始的时候均为1,代表后缀自动机的初始状态
//ch数组类似于trie树的存储结构,代表每一条边
struct Edge{
    int t,nxt;
}edge[maxn];
int head[maxn],cnt1=0;
ll ans=0,z[maxn];
void insert(int x){//x为要插入的字符
    int np=++cnt,p=last;
    len[np]=len[p]+1,last=np,z[cnt]=1;//本题中z代表每一个子串(包括相同本质的子串)出现的个数
    while(p&&!ch[p][x])     ch[p][x]=np,p=fa[p];
    if(!p)  fa[np]=1;
    else{
        int q=ch[p][x];
        if(len[q]==len[p]+1)fa[np]=q;
        else{
            int nq=++cnt;len[nq]=len[p]+1;
            memmove(ch[nq],ch[q],sizeof(ch[nq]));
            fa[nq]=fa[q],fa[np]=fa[q]=nq;
            while(ch[p][x]==q)ch[p][x]=nq,p=fa[p];
        }
    }
}//构建SAM
void link(int a,int b){
    cnt1++;
    edge[cnt1].t=b;
    edge[cnt1].nxt=head[a];
    head[a]=cnt1;
}//用链表结构存每一个节点子串的后缀(存节点,每一个节点就是一个子串)暴力建parent树
void dfs(int node){
    for(int i=head[node];i;i=edge[i].nxt)
        dfs(edge[i].t),z[node]+=z[edge[i].t];
    if(z[node]>1)
        ans=max(ans,z[node]*len[node]);
}
int main(){
    scanf("%s",str),l=strlen(str);
    for(int i=0;i<l;i++){
        int id=str[i]-'a';
        insert(id);//插入字符
    }
    for(int i=2;i<=cnt;i++) 
        link(fa[i],i);//链表结构,有点像暴力键parent树
    dfs(1);//dfs(求得ans)
    printf("%lld",ans);
return 0;
}

一、right集合的定义:

right集合就是指某一子串在原串当中出现的所有的位置的集合,也在某位大佬博客中叫做endpos集合;

二、right集合(endpos集合)相同的我们称为endpos等价类(重点理解)

三、对于任意两个endpos集合中,那么必然为其中一个为另一个的子串,或者两者绝无交集

四、endpos的等价类的数量为O(n)后缀自动的边数也是O(n)

五、我们在求parent树的时候其实是在对endpos的所有等价类进行分割操作,分割出的节点便是parent树上的节点

六、parent树跟后缀自动机节点相同,但是所应用的范围不同:

parent树主要是来求每个节点的性质(或者说字符子串的性质)而SAM其本身主要用于直接跑字符串

同时,我们可以感性的认为,parent树上的连边主要是由于在子串前加上某一字符,而后缀自动机是在某一子串后面加上字符

七、后缀自动机连边的时候,我们认为一个endpos等价类的后面加上一个字符其定然变成了另一个等价类,这样我们就可以来说明后缀自动机的连边是存在的;

你可能感兴趣的:(后缀自动机SAM,字符串,洛谷)