AC自动机

字符串这一方面的算法。
概述:在Trie树上使用KMP算法得到Fail数组,使用Fail数组建树,
那么每次查询的串就是当前位置到Fail树的根得到查询,类似于一个后缀。
利用这个性质以及树上的各种算法可以做某些方面的题。
清空这一方面,一次性全部清空或动态清空均可,考虑到可能会写内存池,采用后者。
在Fail树上可以对当前点的信息或是回答进行记录,部分题目可以均摊复杂度。
https://vjudge.net/contest/202341#overview

T1 WP

类似于模板,分类后使用一个函数来完成某个方向上的答案收集。
可能是看代码长度的题目。

Code
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;

#define Komachi is retarded
#define REP(i,a,b) for(int i=(a),i##_end_=(b);i<i##_end_;i++)
#define DREP(i,a,b) for(int i=(a),i##_end_=(b);i>i##_end_;i--)
#define M 1004
#define N 500004

int n,m,q;
char Map[M][M],C[M];
int Rx[]={-1,-1,0,1,1,1,0,-1},Ry[]={0,1,1,1,0,-1,-1,-1};
struct Node{
    int x,y;
    char c;
    void Pri(int d){
        printf("%d %d %c\n",x-Rx[c-'A']*d,y-Ry[c-'A']*d,c);
    }
};
int Pos[M];
struct Trie{
    int Next[N][26],Fail[N],Dep[N],sz;
    Node Ans[N];
    bool Mark[N];
    void Add(int x){
        scanf("%s",C);
        int p=0;
        REP(i,0,strlen(C)){
            int &nt=Next[p][C[i]-'A'];
            if(!nt)nt=++sz,Dep[nt]=Dep[p]+1;
            p=nt;
        }
        Pos[x]=p;
    }
    int Q[N];
    void ReBuild(){
        int l,r;l=r=0;
        REP(i,0,26)if(Next[0][i])Q[r++]=Next[0][i];
        while(l<r){
            int A=Q[l++];
            REP(i,0,26){
                int &p=Next[A][i];
                if(p)Fail[Q[r++]=p]=Next[Fail[A]][i];
                else p=Next[Fail[A]][i];
            }
        }
    }
    void Answer(int p,Node ans){
        while(p){
            if(Mark[p])return;
            Ans[p]=ans;
            Mark[p]=1;
            p=Fail[p];
        }
    }
    void Answer(int x,int y,int Rx,int Ry,char c){
        int p=0;
        while(x>=0 && y>=0 && x<n && y<m){
            int nt=Next[p][Map[x][y]-'A'];
            p=nt?nt:Fail[p];

            Answer(p,(Node){x,y,c});
            x+=Rx,y+=Ry;
        }
    }
}Trie;

int main(){
    scanf("%d%d%d",&n,&m,&q);
    REP(i,0,n)scanf("%s",Map[i]);

    Trie.Dep[0]=-1;
    REP(i,0,q)Trie.Add(i);
    Trie.ReBuild();

    REP(i,0,n){
        REP(g,0,8)Trie.Answer(i,0,Rx[g],Ry[g],g+'A');
        REP(g,0,8)Trie.Answer(i,m-1,Rx[g],Ry[g],g+'A');
    }
    REP(i,0,m){
        REP(g,0,8)Trie.Answer(0,i,Rx[g],Ry[g],g+'A');
        REP(g,0,8)Trie.Answer(n-1,i,Rx[g],Ry[g],g+'A');
    }

    REP(i,0,q){
        int p=Pos[i];
        Trie.Ans[Pos[i]].Pri(Trie.Dep[Pos[i]]);
    }
    return 0;
}

T2 RA

比较有意思的一道题。
考虑到n的范围小,如果已知两两之间串的拼接代价,可以利用状态压缩DP来求解。
所以用AC自动机将所有串加入,得出无法被访问的节点并标记。
从某个可行串的结尾点出发BFS求解到其他串结尾点的最小距离。
然后回到上一个问题去计算。

Code
#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
using namespace std;

#define Komachi is retarded
#define REP(i,a,b) for(int i=(a),i##_end_=(b);i<i##_end_;i++)
#define M 60004
#define K 1024

#define chkmin(a,b) a=min(a,b)

int n,m,Pos[14],Len[14];
char C[M];
struct AC_Auto{
    int Next[M][2],Fail[M],sz;
    bool Ban[M];
    void Clear(){memset(Next[0],sz=0,sizeof(Next[0]));}
    int NewNode(){sz++;memset(Next[sz],Ban[sz]=Fail[sz]=0,sizeof(Next[sz]));return sz;}
    void Add(int x,bool k){
        scanf("%s",C);
        int p=0;
        REP(i,0,strlen(C)){
            int &nt=Next[p][C[i]-'0'];
            if(!nt)nt=NewNode();
            p=nt;
        }
        if(k)Pos[x]=p,Len[x]=strlen(C);
        else Ban[p]=1;
    }
    int Q[M];
    void ReBuild(){
        int l,r;l=r=0;
        REP(i,0,2)if(Next[0][i])Q[r++]=Next[0][i];
        while(l<r){
            int A=Q[l++];
            REP(i,0,2){
                int &p=Next[A][i];
                if(p)Fail[Q[r++]=p]=Next[Fail[A]][i];
                else p=Next[Fail[A]][i];
            }
            Ban[A]|=Ban[Fail[A]];
        }
    }
    int Lenth[14][14],Dis[M];
    void BFS(int s){
        int l,r;l=r=0;Q[r++]=s;
        memset(Dis,63,sizeof(Dis));
        Dis[s]=0;
        while(l<r){
            int A=Q[l++],B;
            REP(i,0,2)if(Dis[B=Next[A][i]]==0x3f3f3f3f && !Ban[B])
                Q[r++]=B,Dis[B]=Dis[A]+1;
        }
    }
    int DP[14][1044],To[14][14];
    void Answer(){
        memset(To,63,sizeof(To));
        REP(i,0,n){
            BFS(Pos[i]);
            REP(j,0,n)To[i][j]=Dis[Pos[j]];
        }

        memset(DP,63,sizeof(DP));
        REP(i,0,n)DP[i][1<<i]=Len[i];
        REP(i,1,1<<n)REP(j,0,n)if(DP[j][i]!=0x3f3f3f3f){
            REP(k,0,n)chkmin(DP[k][i|(1<<k)],DP[j][i]+To[j][k]);
        }

        int Ans=0x3f3f3f3f;
        REP(i,0,n)Ans=min(Ans,DP[i][(1<<n)-1]);
        printf("%d\n",Ans);
    }
}AC;
int main(){
    while(scanf("%d%d",&n,&m)!=EOF){
        if(!n && !m)break;
        AC.Clear();
        REP(i,0,n)AC.Add(i,1);
        REP(i,0,m)AC.Add(i,0);
        AC.ReBuild();
        AC.Answer();
    }
    return 0;
}

T3 HABT

后缀和前缀的最大匹配长度,
转换到AC自动机上是A串结尾点在Fail树上到根的节点中包含的B串的最长长度。
离线询问,DFS,采用直接修改,记录旧值的方法得到当前栈内的点的答案。
复杂度为 O(n) ,常数可能较大。

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;

#define Komachi is retarded
#define REP(i,a,b) for(int i=(a),i##_end_=(b);i<i##_end_;i++)
#define DREP(i,a,b) for(int i=(a),i##_end_=(b);i>i##_end_;i--)
#define LREP(i,A,E) for(int i=E.Head[A];i;i=E.Next[i])
#define M 100044

inline void chkmax(int &a,const int &b){if(a<b)a=b;}

int n,m,len,L[M],R[M],Nm[444],Len[M],Fr[M],Pos[M];
char C[M];
template<const int Len,const int Num>struct Linklist{
    int Next[Len],Head[Num],Val[Len],tot;
    int operator [](int x){return Val[x];}
    void pb(int u,int v){Next[++tot]=Head[u],Val[Head[u]=tot]=v;}
    void Clear(){memset(Head,tot=0,sizeof(Head));}
};
Linklist<M,M>E,AD,Qes,Rev;
int Tmp[M],B[M],Ans[M],Mx[M];
struct AC_Auto{
    int Next[M][4],Fail[M],sz;
    void Clear(){
        REP(i,0,sz+1)memset(Next[i],Fail[i]=0,sizeof(Next[i]));
        sz=0;
    }
    void Add(int x,int L,int R){
        int p=0,lt=0;
        REP(i,L,R){
            int &nt=Next[p][Nm[C[i]]];
            if(!nt)nt=++sz;
            Len[i]=++lt;
            AD.pb(p=nt,i);
            Fr[i]=x;
        }
        Pos[x]=p;
    }
    int Q[M];
    void ReBuild(){
        int l,r;l=r=0;
        REP(i,0,4)if(Next[0][i])Q[r++]=Next[0][i];
        while(l<r){
            int A=Q[l++];
            REP(i,0,4){
                int &p=Next[A][i];
                if(p)Fail[Q[r++]=p]=Next[Fail[A]][i];
                else p=Next[Fail[A]][i];
            }
            E.pb(Fail[A],A);
        }
    }
}AC;


void Answer(int A){
    LREP(i,A,AD){
        int j=AD[i];
        Tmp[j]=Mx[Fr[j]];
        chkmax(Mx[Fr[j]],Len[j]);
        Rev.pb(A,j);
    }
    LREP(i,A,Qes){
        int j=Qes[i];
        Ans[j]=Mx[B[j]];
    }
    LREP(i,A,E)
        Answer(E[i]);

    LREP(i,A,Rev){
        int j=Rev[i];
        Mx[Fr[j]]=Tmp[j];
    }
}

int main(){
    Nm['A']=0;
    Nm['G']=1;
    Nm['C']=2;
    Nm['T']=3;

    while(scanf("%d%d",&n,&m)!=EOF){
        memset(Mx,0,sizeof(Mx));

        AC.Clear();
        AD.Clear();
        E.Clear();
        Qes.Clear();
        Rev.Clear();
        len=0;

        REP(i,0,n){
            L[i]=len;
            scanf("%s",C+len);
            len+=strlen(C+len);
            R[i]=len;
            AC.Add(i,L[i],R[i]);
        }

        REP(i,0,m){
            int a;
            scanf("%d%d",&a,&B[i]);
            a--,B[i]--;
            Qes.pb(Pos[a],i);
        }

        AC.ReBuild();

        Answer(0);

        REP(i,0,m)printf("%d\n",Ans[i]);
    }
    return 0;
}

T4 GREW

在Fail树上DP,线段树优化查询。

Code
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;

#define Komachi is retarded
#define REP(i,a,b) for(int i=(a),i##_end_=(b);i<i##_end_;i++)
#define DREP(i,a,b) for(int i=(a),i##_end_=(b);i>i##_end_;i--)
#define LREP(i,A,E) for(int i=E.Head[A];i;i=E.Next[i])
#define M 300004

inline void chkmax(int &a,const int &b){if(a<b)a=b;}

int T,n,m,len,L[M],R[M],W[M],Pos[M],Ans,DFN[M],Ed[M],dfn;
char C[M];
template<const int Len,const int Num>struct Linklist{
    int Next[Len],Head[Num],Val[Len],tot;
    int operator [](int x){return Val[x];}
    void pb(int u,int v){Next[++tot]=Head[u],Val[Head[u]=tot]=v;}
    void Clear(){memset(Head,tot=0,sizeof(Head));}
};
Linklist<M,M>E;

struct Segtree{
    #define lp (p<<1)
    #define rp (p<<1|1)
    #define lson l,mid,lp
    #define rson mid+1,r,rp

    int Mx[M<<2];
    void Clear(){memset(Mx,0,sizeof(Mx));}
    void Updata(int l,int r,int p,int a,int b,int t){
        if(l>b || r<a)return;
        if(a<=l&&r<=b){chkmax(Mx[p],t);return;}
        int mid=l+r>>1;
        Updata(lson,a,b,t);
        Updata(rson,a,b,t);
    }
    int Query(int l,int r,int p,int a,int b){
        if(l>b || r<a)return 0;
        if(a<=l&&r<=b)return Mx[p];
        int mid=l+r>>1;
        return max(Mx[p],max(Query(lson,a,b),Query(rson,a,b)));
    }
}Tree;
struct AC_Auto{
    int Next[M][26],Fail[M],sz;
    void Clear(){
        REP(i,0,sz+1)memset(Next[i],Fail[i]=0,sizeof(Next[i]));
        sz=0;
    }
    void Add(int x,int L,int R){
        int p=0;
        REP(i,L,R){
            int &nt=Next[p][C[i]-'a'];
            if(!nt)nt=++sz;
            p=nt;
        }
        Pos[x]=p;
    }
    int Q[M];
    void ReBuild(){
        int l,r;l=r=0;
        REP(i,0,26)if(Next[0][i])Q[r++]=Next[0][i];
        while(l<r){
            int A=Q[l++];
            REP(i,0,26){
                int &p=Next[A][i];
                if(p)Fail[Q[r++]=p]=Next[Fail[A]][i];
                else p=Next[Fail[A]][i];
            }
            E.pb(Fail[A],A);
        }
    }
}AC;
void DFS(int A){
    DFN[A]=++dfn;
    LREP(i,A,E)DFS(E[i]);
    Ed[A]=dfn;
}
int main(){
    scanf("%d",&T);
    REP(Case,1,T+1){
        Ans=dfn=len=0;
        AC.Clear();
        E.Clear();
        Tree.Clear();

        scanf("%d",&n);

        REP(i,0,n){
            L[i]=len;
            scanf("%s",C+len);
            len+=strlen(C+len);
            R[i]=len;
            AC.Add(i,L[i],R[i]);

            scanf("%d",&W[i]);
        }
        AC.ReBuild();
        DFS(0);

        REP(i,0,n){
            int A=Pos[i];
            int DP=0,p=0;
            REP(j,L[i],R[i]){
                p=AC.Next[p][C[j]-'a'];
                chkmax(DP,Tree.Query(1,AC.sz+1,1,DFN[p],DFN[p]));
            }
            Tree.Updata(1,AC.sz+1,1,DFN[A],Ed[A],DP+W[i]);
            chkmax(Ans,DP+W[i]);
        }

        printf("Case #%d: %d\n",Case,Ans);
    }
    return 0;
}

T5 S

按照上面的方法来的话写起来比较诡异。
查询前缀+后缀,即在后缀的结尾点上在Fail树上的子树那些点有该前缀的点数。
Hash去重,在Fail树上用Dsu维护Trie树可以写出来。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<map>
using namespace std;

#define Komachi is retarded
#define REP(i,a,b) for(int i=(a),i##_end_=(b);i<i##_end_;i++)
#define DREP(i,a,b) for(int i=(a),i##_end_=(b);i>i##_end_;i--)
#define LREP(i,A,E) for(int i=E.Head[A];i;i=E.Next[i])
#define M 500044
#define ULL unsigned long long
#define Base 233

map<ULL,int>Mark;
ULL Bc[M],H1[M],H2[M];

ULL Hash(ULL *H,int l,int r){
    return H[r]-H[l-1]*Bc[r-l+1];
}

int T,n,m,len,Beg[M],Pr[M],Ans[M];
char C[M],Prx[M],Sux[M];

template<const int Len,const int Num>struct Linklist{
    int Next[Len],Head[Num],Val[Len],tot;
    int operator [](int x){return Val[x];}
    void pb(int u,int v){Next[++tot]=Head[u],Val[Head[u]=tot]=v;}
    void Clear(){memset(Head,tot=0,sizeof(Head));}
};
Linklist<M,M>E,D,Qes;
struct AC_Auto{
    int Next[M<<1][26],Fail[M<<1],sz;
    int Cnt[M<<1];
    void Clear(){memset(Next[0],sz=0,sizeof(Next[0]));}
    int NewNode(){
        sz++;
        memset(Next[sz],Fail[sz]=Cnt[sz]=0,sizeof(Next[sz]));
        return sz;
    }
    int Add(int x){
        int p=0;
        REP(i,Beg[x],Beg[x+1]){
            int &nt=Next[p][C[i]-'a'];
            if(!nt)nt=NewNode();
            Cnt[p=nt]++;
        }
        return p;
    }
    int Q[M<<1];
    void ReBuild(){
        int l,r;l=r=0;
        REP(i,0,26)if(Next[0][i])Q[r++]=Next[0][i];
        while(l<r){
            int A=Q[l++];
            REP(i,0,26){
                int &p=Next[A][i];
                if(p)Fail[Q[r++]=p]=Next[Fail[A]][i];
                else p=Next[Fail[A]][i];
            }
            E.pb(Fail[A],A);
        }
    }
    void Query(int x){
        int p=0;
        REP(i,0,strlen(Sux)){
            int &nt=Next[p][Sux[i]-'a'];
            if(!nt)nt=NewNode();
            p=nt;
        }
        Qes.pb(p,x);
    }
    int QueryC(int x){
        int p=0;
        REP(i,Pr[x],Pr[x+1]){
            int nt=Next[p][Prx[i]-'a'];
            if(!nt)return 0;
            p=nt;
        }
        return Cnt[p];
    }
}AC,Trie;

int Sz[M],Son[M];
void Get(int A){
    int B;
    Sz[A]=0;
    LREP(i,A,D)Sz[A]++;
    Son[A]=M-1;
    LREP(i,A,E){
        Get(B=E[i]);
        Sz[A]+=Sz[B];
        if(Sz[B]>Sz[Son[A]])Son[A]=B;
    }
}
void Add(int A){
    LREP(i,A,D)Trie.Add(D[i]);
    LREP(i,A,E)Add(E[i]);
}
void Answer(int A){
    int B;
    LREP(i,A,E)if((B=E[i])!=Son[A])
        Answer(B),Trie.Clear();

    if(Son[A]!=M-1)Answer(Son[A]);

    LREP(i,A,E)if((B=E[i])!=Son[A])Add(B);
    LREP(i,A,D)Trie.Add(D[i]);

    LREP(i,A,Qes){
        int j=Qes[i];
        Ans[j]+=Trie.QueryC(j);
    }
}
int main(){
    Bc[0]=1;
    REP(i,1,M)Bc[i]=Bc[i-1]*Base;

    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&m);

        len=0;
        AC.Clear();
        E.Clear();
        D.Clear();
        Qes.Clear();
        Mark.clear();
        memset(Ans,0,sizeof(Ans));

        REP(i,0,n){
            scanf("%s",C+len);
            len+=strlen(C+len);
            Beg[i+1]=len;
            D.pb(AC.Add(i),i);
        }
        REP(i,0,n){
            ULL Val=0;
            REP(j,Beg[i],Beg[i+1])
                Val=Val*Base+C[j];
            Mark[Val]++;
        }

        len=0;
        REP(i,0,m){
            int a,b;

            scanf("%s%s",Prx+len,Sux);

            a=strlen(Prx+len);
            b=strlen(Sux);

            Pr[i+1]=len+a;

            AC.Query(i);

            REP(j,0,a)H1[j+1]=H1[j]*Base+Prx[len+j];
            REP(j,0,b)H2[j+1]=H2[j]*Base+Sux[j];

            REP(j,1,min(a,b)+1)if(Hash(H1,a-j+1,a)==Hash(H2,1,j))
                Ans[i]-=Mark[Hash(H1,1,a-j)*Bc[b]+H2[b]];

            len+=a;
        }

        AC.ReBuild();

        Trie.Clear();
        Get(0);
        Answer(0);

        REP(i,0,m) printf("%d\n",Ans[i]);
    }
    return 0;
}

另外有一种比较巧妙的方法。
将查询在前缀和后缀中插入一个特殊字符并拼接来作为模式串。
将所有原来的串用 S+特殊字符+S 拼接来作为主串。
所有主串均进行一次匹配,结束后再Hash去重即可。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<map>
using namespace std;

#define Komachi is retarded
#define REP(i,a,b) for(int i=(a),i##_end_=(b);i<i##_end_;i++)
#define DREP(i,a,b) for(int i=(a),i##_end_=(b);i>i##_end_;i--)
#define LREP(i,A,E) for(int i=E.Head[A];i;i=E.Next[i])
#define M 604444
#define ULL unsigned long long
#define Base 233

map<ULL,int>Mark;
ULL Bc[M],H1[M],H2[M];

ULL Hash(ULL *H,int l,int r){
    return H[r]-H[l-1]*Bc[r-l+1];
}

int T,n,q,len,Beg[M],Ans[M],Pos[M];
char C[M<<1],Prx[M<<1],Sux[M];

template<const int Len,const int Num>struct Linklist{
    int Next[Len],Head[Num],Val[Len],tot;
    int operator [](int x){return Val[x];}
    void pb(int u,int v){Next[++tot]=Head[u],Val[Head[u]=tot]=v;}
    void Clear(){memset(Head,tot=0,sizeof(Head));}
};
Linklist<M,M>E;

struct AC_Auto{
    int Next[M][27],Fail[M],sz;
    int Cnt[M];
    void Clear(){memset(Next[0],sz=0,sizeof(Next[0]));}
    int NewNode(){
        sz++;
        memset(Next[sz],Cnt[sz]=Fail[sz]=0,sizeof(Next[sz]));
        return sz;
    }
    void Add(int x,int l){
        int p=0;
        REP(i,0,l){
            int &nt=Next[p][Sux[i]-'a'];
            if(!nt)nt=NewNode();
            p=nt;
        }
        Pos[x]=p;
    }
    int Q[M];
    void ReBuild(){
        int l,r;l=r=0;
        REP(i,0,27)if(Next[0][i])Q[r++]=Next[0][i];
        while(l<r){
            int A=Q[l++];
            REP(i,0,27){
                int &p=Next[A][i];
                if(p)Fail[Q[r++]=p]=Next[Fail[A]][i];
                else p=Next[Fail[A]][i];
            }
            E.pb(Fail[A],A);
        }
    }
    void Answer(int x){
        int p=0;
        REP(i,Beg[x],Beg[x+1])Cnt[p=Next[p][C[i]-'a']]++;
    }
    void DFS(int A){
        LREP(i,A,E)DFS(E[i]),Cnt[A]+=Cnt[E[i]];
    }
}AC;
int main(){
    Bc[0]=1;
    REP(i,1,M)Bc[i]=Bc[i-1]*Base;

    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&q);

        Mark.clear();
        memset(Ans,len=0,sizeof(Ans));
        AC.Clear();
        E.Clear();

        REP(i,0,n){
            scanf("%s",C+len);
            int m=strlen(C+len);

            len+=m;
            C[len++]=26+'a';

            ULL Val=0;
            REP(j,0,m)Val=Val*Base+(C[len++]=C[Beg[i]+j]);

            Beg[i+1]=len;

            Mark[Val]++;
        }

        REP(i,0,q){
            scanf("%s%s",Prx,Sux);
            int a=strlen(Prx),b=strlen(Sux);

            REP(j,0,a)H1[j+1]=H1[j]*Base+Prx[j];
            REP(j,0,b)H2[j+1]=H2[j]*Base+Sux[j];

            REP(j,1,min(a,b)+1)if(Hash(H1,a-j+1,a)==Hash(H2,1,j))
                Ans[i]-=Mark[Hash(H1,1,a-j)*Bc[b]+H2[b]];

            Sux[b]=26+'a';
            REP(j,0,a)Sux[b+j+1]=Prx[j];

            AC.Add(i,a+b+1);
        }

        AC.ReBuild();
        REP(i,0,n)AC.Answer(i);

        AC.DFS(0);

        REP(i,0,q)printf("%d\n",Ans[i]+AC.Cnt[Pos[i]]);
    }
    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值