Loading

广义后缀自动机

广义后缀自动机

感觉挺水的,大致上和后缀自动机没有什么区别,只是加了几句特判。

1 离线做法

我们先对所有的字符串建一棵 Trie 树,然后对这颗 Trie 树建立后缀自动机。

如何建立呢?我们只需要对这颗 Trie 树进行 bfs 或 dfs ,不断往自动机里面加点就可以了。

唯一不同的是,节点的 \(last\) 要为其父节点,所以要储存 Trie 树上每一个节点在后缀自动机上的编号。

注意,如果是给了你一棵 Trie 树,那么 dfs 做法可能会被卡成 \(O(n^2)\) ,其中 \(n\) 是所有字符串的长度总和。

至于为什么会被卡,感兴趣的读者可以看我在引用部分挂的链接,这里不再赘述。

插入部分和普通 SAM 的插入相同。

#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 5000100
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

struct SAM{
    struct node{
        int link,ch[26],len;
    };
    node p[N];
    int size,tr[N][26],pos[N],c[N],fa[N];
    queue<int> q;
    inline SAM(){p[0].len=0;p[0].link=-1;}
    inline void trie_insert(char *s){
        int now=0,len=strlen(s);
        for(int i=0;i<len;i++){
            if(!tr[now][s[i]-'a']) {tr[now][s[i]-'a']=++size;c[size]=s[i]-'a';fa[size]=now;}
            now=tr[now][s[i]-'a'];
        }
    }
    inline int insert(int c,int last){
        int cur=++size,now=last;
        p[cur].len=p[now].len+1;
        while(now!=-1&&!p[now].ch[c]) p[now].ch[c]=cur,now=p[now].link;
        if(now==-1) p[cur].link=0;
        else{
            int q=p[now].ch[c];
            if(p[q].len==p[now].len+1) p[cur].link=q;
            else{
                int clone=++size;
                p[clone]=p[q];p[clone].len=p[now].len+1;
                while(now!=-1&&p[now].ch[c]==q) p[now].ch[c]=clone,now=p[now].link;
                p[q].link=p[cur].link=clone;
            }
        }
        return cur;
    }
    inline void build(){
        for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
        pos[1]=0;
        while(q.size()){
            int top=q.front();q.pop();
            pos[top]=insert(c[top],pos[fa[top]]);
            for(int i=0;i<26;i++) if(tr[top][i]) q.push(tr[top][i]);
        }
    }
    inline ll ask_sum(){
        ll res=0;
        for(int i=1;i<=size;i++) res+=p[i].len-p[p[i].link].len;
        return res;
    }
};
SAM sam;

int n;
char s[N];

int main(){
    read(n);
    for(int i=1;i<=n;i++){
        scanf("%s",s);sam.trie_insert(s);
    }
    sam.build();
    ll ans=sam.ask_sum();
    printf("%lld\n",ans);
    return 0;
}

这个做法的正确性来自于 Trie 树本身就储存了大量的 lcp 信息,每一个放进去的节点都可以看做是一个被压缩的子串,所以正确性感性理解一下应该是比较显然的。

2 在线做法

给定 \(n\) 个字符串 \(s_1,s_2,...s_n\) ,要求我们在线构造广义后缀自动机。

首先我们每加入一个串,\(last\) 都要重置到自动机源点,这个比较好理解,毕竟这些串不是连起来的。

那么我们这样做会带来一个问题,就是 ch[last][c] 可能已经有数了,这个怎么办?

我们先放代码:

#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 3000100
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

struct node{
    int link,ch[26],len;
};
node p[N];

struct SAM{
    int size,last;
    inline SAM(){p[0].link=-1;p[0].len=0;last=0;}
    inline void add(char *s){
        last=0;int len=strlen(s);
        for(int i=0;i<len;i++) last=insert(s[i]-'a');
    }
    inline int insert(int c){
        if(p[last].ch[c]){
            int q=p[last].ch[c],now=last;
            if(p[now].len+1==p[q].len) return q;
            else{
                int clone=++size;
                p[clone]=p[q];p[clone].len=p[now].len+1;
                while(now!=-1&&p[now].ch[c]==q) p[now].ch[c]=clone,now=p[now].link;
                p[q].link=clone;
                return clone;
            }
        }
        int cur=++size;p[size].len=p[last].len+1;
        int now=last;
        while(now!=-1&&!p[now].ch[c]) p[now].ch[c]=cur,now=p[now].link;
        if(now==-1) p[cur].link=0;
        else{
            int q=p[now].ch[c];
            if(p[now].len+1==p[q].len) p[cur].link=q;
            else{
                int clone=++size;
                p[clone]=p[q];p[clone].len=p[now].len+1;
                while(now!=-1&&p[now].ch[c]==q) p[now].ch[c]=clone,now=p[now].link;
                p[q].link=p[cur].link=clone;
            }
        }
        return cur;
    }
    inline ll ask_sum(){
        ll res=0;
        for(int i=1;i<=size;i++){
            res+=p[i].len-p[p[i].link].len;
        }
        return res;
    }
};
SAM sam;

ll n,ans;
char s[N];

int main(){
    read(n);
    for(int i=1;i<=n;i++){
        scanf("%s",s);sam.add(s);
    }
    ans=sam.ask_sum();
    printf("%lld\n",ans);
    return 0;
}

相信读者已经发现,我们的特判与我们后缀自动机拆点的步骤非常相像。而特判的本质是看一看是否有必要加入这个节点。如果你已经十分了解后缀自动机了,那么其实如果有 p[now].len+1==p[q].len ,那么 \(q\) 实际上已经把我们将要加入的这个节点完全替代了。

否则,那我们需要再建立一个 \(len\)p[now].len+1 的节点,发现这个问题和我们拆点的问题非常像,所以我们可以直接参考我们在拆点那里的代码。

3 复杂度分析

根据后缀自动机的复杂度,这个复杂度应该是线性的,在实际实现中,因为离线算法要建 Trie 树,所以离线算法要比在线算法常数大。

如果广义后缀自动机构建正确,离线算法和在线算法构造的时间复杂度应该是相同的。

4 引用

链接

posted @ 2021-07-09 10:54  hyl天梦  阅读(75)  评论(0编辑  收藏  举报