AC自动机fail树

AC自动机

这是一个处理字符串的算法,主要是处理有若干模式串,一个文本串的时候我们如何高效 \(O(\sum |S|+|S'|)\) 的匹配的问题。

我们考虑将每一个模式串建在一棵 trie 树上。首先,我们 trie 上的每一个节点都代表这从 trie 的根走下来的一个字符串。我们定义 fail 数组表示我现在表示的字符串中,我愿意舍弃我的若干前缀,与同样在 trie 树里存在的一个字符串一模一样的最长一部分的东西的指向那个字符串的编号。这看起来十分拗口而且非常难理解,但是我们举一个例子帮助一下理解。

假如说我们现在只有一个串 \(aaaa\)

对于第一个,这个 trie 树还是空的,肯定没有,第二个,fail 就指向了第一个,第三个 fail 指向第二个,第四个就指向第三个。

构建了这样的一个数据结构有什么用呢?现在假设我们有一个文本串来了,让我们匹配。我们可以考虑一下在这个 trie 上走一遍,对于每一个节点,我们就走一遍 fail 指针,然后把走过的东西全部累加。为什么可以这么做?因为我出现了,我的 fail 指针指向的东西一定也出现了,同时,因为 fail 指针满足走向的东西是最长的前缀,这样的话我们把 fail 走到尽头,肯定会出现每一个我们的模式串。

如何求 fail 指针呢?

如果一个节点的父亲的 fail 指针已经求好了,我们现在要求它自己的 fail 指针,这件事情是不难的,考虑它父亲的 fail 指针指向的东西有没有跟他它一样的儿子,如果有,就把它指向它。这是显然的,但是如果没有怎么办,这里又是一个非常巧妙的构造,如果这个节点下面没有某一个字符,我们把它的 fail 指针下的一个字符连向他。这样的话就不用循环跳 fail 了,毕竟显然的一点是 fail 的长度肯定是越来越长的。所以从小的到大的,每一个可以连的我都尝试过了,再没有这个字符就是整个图里没有这个字符了。

那么这就是 AC 自动机了,这里再讲其的一个优化,拓扑建图优化。这是一个非常显然的优化,就是如果我们每次 Query 都是暴力跳 fail 这肯定是不优的,我们可以记录我们跳过这个节点多少次,然后最后我们按照拓扑序来一遍就可以了。

fail 树

前缀是一个很好的性质,因为我的前缀的前缀肯定还是我的前缀,它是具有传递性的。

fail树就是利用这个性质完成一系列操作的一个数据结构(?我们把在 trie 图上他的 fail 指针单独掏出来变成一个树。这一定是一棵树,并且满足我一条边 \(u\rightarrow v\) 肯定满足 \(u\)\(v\) 的前缀。这个性质是很好的。

我们来看一道例题:

P5840 divljak

\(n\) 个模式串一开始就给你了,文本串集合支持增加,求某一个模式串在文本串集合里有多少文本串包含他。

一开始读错题目了,再加上这题过于阴间,所以调了一个上午。

首先,因为有多个模式串,所以我们还是把 AC 自动机建好。

我们考虑每一个文本串可能造成的贡献,如果我们按照普通的 AC 自动机走一遍,每走一遍都是 \(O(n)\) 的,肯定爆炸。那么我们有什么办法去维护这个东西么。每一次,我们记录答案肯定是在 fail 构成的树上跳来跳去,我们考虑把 fail 树建出来。

建出来之后,我们在原 AC 自动机上跳一跳会发现,对于 fail 树上的一个节点有答案贡献的话,对从根节点开始到这条路径上的所有节点都肯定有贡献。是不是这样就可以了呢,当然不是。比如说如果我有一个 \(aaa\) 我现在来一个 \(aaab\) 难不成我再加上一遍 \(b\) 那么我们可以减去根到 lca 的值。这样的话就平衡了。

所以只要写一个树剖、AC 自动机、线段树就可以了。调了一个上午。

#include <bits/stdc++.h>
#define debug puts("????");
using namespace std;
template <typename T>inline void read(T& t){t=0; register char ch=getchar(); register int fflag=1;while(!('0'<=ch&&ch<='9')) {if(ch=='-') fflag=-1;ch=getchar();}while(('0'<=ch&&ch<='9')){t=t*10+ch-'0'; ch=getchar();} t*=fflag;}
template <typename T,typename... Args> inline void read(T& t, Args&... args) {read(t);read(args...);}
const int N=1e7+10, inf=0x3f3f3f3f;

int n,trie[N][27],cnt,Map[N],fail[N];
bool flag[N];

template<typename T>
struct Segment_Tree{
    int lc[N],rc[N];
    T sum[N],tag[N];
    void pushup(int u){
        sum[u]=sum[u<<1]+sum[u<<1|1];
    }
    void pushdown(int u){
        if(tag[u]){
            tag[u<<1]+=tag[u];
            tag[u<<1|1]+=tag[u];
            sum[u<<1]+=(rc[u<<1]-lc[u<<1]+1)*tag[u];
            sum[u<<1|1]+=(rc[u<<1|1]-lc[u<<1|1]+1)*tag[u];
            tag[u]=0;
        }
    }
    void build(int u,int l,int r){
        lc[u]=l;rc[u]=r;
        if(l==r) return;
        int mid=l+r>>1;
        build(u<<1,l,mid);build(u<<1|1,mid+1,r);
        pushup(u);
    }
    void update(int u,int l,int r,int val){
        if(l<=lc[u]&&r>=rc[u]){
            sum[u]+=(rc[u]-lc[u]+1)*val;
            tag[u]+=val;
            return;
        }
        if(l>rc[u]||lc[u]>r) return;
        pushdown(u);
        update(u<<1,l,r,val); update(u<<1|1,l,r,val);
        pushup(u);
    }
    int Query(int u,int l,int r){
        if(l<=lc[u]&&r>=rc[u]) return sum[u];
        if(l>rc[u]||lc[u]>r) return 0;
        pushdown(u);
        return Query(u<<1,l,r)+Query(u<<1|1,l,r);
    }
};
Segment_Tree<int>seg;

void update(string st,int id){
    int u=0;
    for(int i=0;i<st.size();++i){
        if(!trie[u][st[i]-'a']) trie[u][st[i]-'a']=++cnt;
        u=trie[u][st[i]-'a'];
    }
    Map[id]=u;
}
void getfail(){
    queue<int>Q;
    for(int i=0;i<26;++i) if(trie[0][i]) Q.push(trie[0][i]);
    while(!Q.empty()){
        int u=Q.front(); Q.pop();
        for(int i=0;i<26;++i){
            if(trie[u][i]){
                fail[trie[u][i]]=trie[fail[u]][i];
                Q.push(trie[u][i]);
            }else{
                trie[u][i]=trie[fail[u]][i];
            }
        }
    }
}
int sz[N],zr[N],dep[N],fa[N],top[N],id[N];
vector<int>G[N];
void dfs(int u,int F){
    sz[u]=1; fa[u]=F; dep[u]=dep[F]+1;
    for(int v:G[u]){
        if(v==F) continue;
        dfs(v,u);
        sz[u]+=sz[v];
        if(zr[u]==-1||sz[v]>sz[zr[u]]) zr[u]=v;
    }
}
int tot;
void dfs1(int u,int tp){
    top[u]=tp; id[u]=++tot;
    if(zr[u]!=-1) dfs1(zr[u],tp);
    for(int v:G[u]){
        if(v==zr[u]||v==fa[u]) continue;
        dfs1(v,v);
    }
}
void update(int x,int y,int val){
    while(top[x]!=top[y]){
        if(dep[top[x]]>dep[top[y]]) swap(x,y);
        seg.update(1,id[top[y]],id[y],val);
        y=fa[top[y]];
    }
    if(dep[x]>dep[y]) swap(x,y);
    seg.update(1,id[x],id[y],val);
}
int LCA(int x,int y){
    while(top[x]!=top[y]){
        if(dep[top[x]]>dep[top[y]]) swap(x,y);
        y=fa[top[y]];
    }
    if(dep[x]>dep[y]) return y;
    else return x;
}
bool cmp(int a,int b){
    return id[a]<id[b];
}
void change(string st){
    int u=0,ru=0,cnt1=0;
    static int pt[N];
    for(int i=0;i<st.size();++i){
        u=trie[u][st[i]-'a'];
        pt[++cnt1]=u;
    }
    sort(pt+1,pt+cnt1+1,cmp);
    cnt1=unique(pt+1,pt+cnt1+1)-pt;
    sort(pt+1,pt+cnt1+1,cmp);
    for(int i=1;i<=cnt1;++i) update(pt[i],0,1);
    for(int i=1;i<cnt1;++i) update(LCA(pt[i],pt[i+1]),0,-1);
    for(int i=1;i<=st.size()+1;++i) pt[i]=0;
}

signed main(){
    read(n);
    memset(zr,-1,sizeof(zr));
    for(int i=1;i<=n;++i){
        string st;
        cin>>st;
        update(st,i);
    }
    getfail();
    for(int i=1;i<=cnt;++i) G[fail[i]].push_back(i);
    dfs(0,-1); dfs1(0,0);
    seg.build(1,1,cnt+1);
    int q;
    read(q);
    while(q--){
        int opt;
        read(opt);
        if(opt==1){
            string st;
            cin>>st;
            change(st);
        }else{
            int val;
            read(val);
            cout<<seg.Query(1,id[Map[val]],id[Map[val]])<<endl;
        }
    }
    return 0;
}
/*
是谁挥霍的时光啊,是谁苦苦的奢望啊

这不是一个问题,也不需要你的回答

No answer. 
*/
posted @ 2022-08-22 12:49  Mercury_City  阅读(66)  评论(0编辑  收藏  举报