AC自动机

时间有限,只过了板子和少数题目。。在应该搞DP的时候搞这个。。

关于AC自动机的介绍,LuoGu日报讲的挺好的,可以比较好的入门。

一、模板

 

#include<bits/stdc++.h>
#define RG register
#define IL inline
#define LL long long
using namespace std;

IL int gi () {
    RG int x=0,w=0; char ch=0;
    while (ch<'0'||ch>'9') {if (ch=='-') w=1;ch=getchar();}
    while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return w?-x:x;
}

const int N=1e6+10;

int n,ans,tot[210];
char str[N],ss[210][110];

namespace AC_Automaton {
    queue <int> q;
    int num,cnt[N],f[N],tr[N][27];

    IL void New () {
        num=0;
        while (!q.empty()) q.pop();
        memset(f,0,sizeof(f));
        memset(tr,0,sizeof(tr));
        memset(cnt,0,sizeof(cnt));
    }
    
    IL void Insert (char s[],int id) {
        RG int i,p=0,now,len=strlen(s);
        for (i=0;i<len;++i) {
            now=s[i]-'a'+1;
            if (!tr[p][now]) tr[p][now]=++num;
            p=tr[p][now];
        }
        cnt[p]=id;
    }          
    
    IL void Get_Fail () {
        RG int i,p;
        for (i=1;i<=26;++i)
            if (tr[0][i]) q.push(tr[0][i]);
        while (!q.empty()) {
            p=q.front(),q.pop();
            for (i=1;i<=26;++i) 
                if (tr[p][i]) f[tr[p][i]]=tr[f[p]][i],q.push(tr[p][i]);
                else tr[p][i]=tr[f[p]][i];
                // *类似并查集路径压缩*
                // *所以可以一步到位*
        }
    }

    IL void Search (char s[]) {
        int i,j,p=0,len=strlen(s);
        for (i=0;i<len;++i) {
            p=tr[p][s[i]-'a'+1];
            for (j=p;j;j=f[j])
                if (cnt[j]) ++tot[cnt[j]];
        }
    }
}
using namespace AC_Automaton;

int main ()
{
    RG int i;
    while ((n=gi())!=0) {
        ans=0,New();
        memset(tot,0,sizeof(tot));
        for (i=1;i<=n;++i)
            scanf("%s",ss[i]),Insert(ss[i],i);
        scanf("%s",str); Get_Fail();
        Search(str);
        for (i=1;i<=n;++i)
            if (tot[i]>ans) ans=tot[i];
        printf("%d\n",ans);
        for (i=1;i<=n;++i)
            if (tot[i]==ans) printf("%s\n",ss[i]);
    }
    return 0;
}
BY BHLLX

我的不知道为什么跑的贼慢。。。

 

二、LuoguP2444病毒

硬是不会rand一个puts??

对于一个刚入门AC自动机的人来说,这个题还是有点意思的。。

先建一颗AC自动机,每个串结尾打上标记。

我们这样考虑一下:

如果我们拿最后那个合法的序列去AC自动机上匹配,那么我们会发现它恰好能避开所有打了标记的节点,通过fail指针在AC自动机上打转。

那么我们是不是只要在这个AC自动机上找到一个不含任何病毒节点的环就好了??

怎么找??dfs找就好了,先看代码。

void dfs (int x) {
    if (pth[x]) {puts("TAK");exit(0);}
    if (vir[x]||vis[x]) return;//vir是病毒标记
    pth[x]=vis[x]=1;
    dfs(tr[x][0]),dfs(tr[x][1]);
    pth[x]=0;
}

 

开两个数组分别代表 当前路径已经过、历史曾访问过,用于找环。

另:我们发现好像是一直沿着trie树往下走,实则不然,因为我们构造fail时,已经把 每个节点没有的那个子节点 和 它父亲的fail指向的节点 的这个节点相连,即类似并查集

的路径压缩操作的那一步,所以其实它是在这颗trie树上跳来跳去的。。。(根据样例画图理解最佳)

 

#include<bits/stdc++.h>
#define RG register
#define IL inline
#define LL long long
using namespace std;

IL int gi () {
    RG int x=0,w=0; char ch=0;
    while (ch<'0'||ch>'9') {if (ch=='-') w=1;ch=getchar();}
    while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return w?-x:x;
}

const int N=3e4+10;

queue <int> q;
char str[N];
int n,num,f[N],vir[N],tr[N][2],pth[N],vis[N];

IL void Insert (char *s) {
    RG int i,p=0,now,len=strlen(s);
    for (i=0;i<len;++i) {
        now=str[i]^48;
        if (!tr[p][now]) tr[p][now]=++num;
        p=tr[p][now];
    }
    vir[p]=1;
}

IL void Get_Fail () {
    RG int i,x,now;
    if (tr[0][0]) q.push(tr[0][0]);
    if (tr[0][1]) q.push(tr[0][1]);
    while (!q.empty()) {
        x=q.front(),q.pop();
        for (i=0;i<2;++i) {
            now=tr[x][i];
            if (!now) tr[x][i]=tr[f[x]][i];
            else f[now]=tr[f[x]][i],vir[now]|=vir[f[now]],q.push(now);    
        }
    }
}

void dfs (int x) {
    if (pth[x]) {puts("TAK");exit(0);}
    if (vir[x]||vis[x]) return;
    pth[x]=vis[x]=1;
    dfs(tr[x][0]),dfs(tr[x][1]);
    pth[x]=0;
}

int main ()
{
    RG int i;
    n=gi();
    for (i=1;i<=n;++i)
        scanf("%s",str),Insert(str);
    Get_Fail(),dfs(0);
    puts("NIE");   
    return 0;
}
BY BHLLX

 

三、luoguP2414阿狸的打字机

刚了差不多一天。。。题是真的一道好题。。。

详细介绍可以看yyb神仙写的博客,写的很好。

这种题先打一个暴力,70分,还挺好想。。(然而我还是看了yyb神仙的代码调了一波)

 

#include<bits/stdc++.h>
#define RG register
#define IL inline
#define LL long long
using namespace std;

IL int gi () {
    int x=0; char ch=0;
    while (ch<'0'||ch>'9') ch=getchar();
    while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return x;
}

const int N=1e5+10;

queue <int> q;
char str[N],ss[N];
int n,num,wd[N],f[N],tr[N][27],fa[N],End[N],cnt[N],match[N],ans[N];

struct Query {int id,x,y;}qur[N];
IL bool cmp (Query A,Query B) {return A.y<B.y;}

IL void Insert (char *s,int len,int id) {
    RG int i,p=0,now;
    for (i=0;i<len;++i) {
        now=s[i]-'a'+1;
        if (!tr[p][now]) tr[p][now]=++num;
        fa[tr[p][now]]=p,p=tr[p][now];
    }
    End[p]=1,wd[id]=p;
}

IL void Get_Fail () {
    RG int i,x,now;
    for (i=1;i<=26;++i)
        if (tr[0][i]) q.push(tr[0][i]);
    while (!q.empty()) {
        x=q.front(),q.pop();
        for (i=1;i<=26;++i) {
            now=tr[x][i];
            if (!now) tr[x][i]=tr[f[x]][i];
            else f[now]=tr[f[x]][i],q.push(now);
        }
    }
}

IL void Search (int id) {
    RG int i,now=wd[id];
    while (now) {
        for (i=now;i;i=f[i])
            if (End[i]) ++cnt[i];
        now=fa[now];
    }
}

int main ()
{
    RG int i,j,k,x,y,len,id;
    scanf("%s",str),n=gi(),len=strlen(str);
    for (i=1;i<=n;++i) x=gi(),y=gi(),qur[i]=(Query){i,x,y};
    sort(qur+1,qur+n+1,cmp);
    for (i=0,j=0,id=0;i<len;++i) {
        if (str[i]=='P') Insert(ss,j,++id);
        else if (str[i]=='B') --j;
        else ss[j++]=str[i];
    }
    Get_Fail();
    for (i=1;i<=n;++i) {
        if (!match[qur[i].y]) {
            match[qur[i].y]=1;
            memset(cnt,0,sizeof(cnt));
            Search(qur[i].y);
        }
        ans[qur[i].id]=cnt[wd[qur[i].x]];
    }
    for (i=1;i<=n;++i) printf("%d\n",ans[i]);
    return 0;
}
BY BHLLX

 

上述代码有一个很耗时的问题,就是每次都重新插入,其实可以记录一下每个节点的父亲,就可以很快的继续执行插入。不然读入都会超时。

然后我们怎么考虑这个问题:

我们发现是求一个串x在另一个串y中出现了多少次。

反过来考虑的话就是分别求出:对于每一个y它能给对应的x的答案产生多少贡献

这个怎么求呢?我们要利用到fail(这个东西真的很神仙)。

一个节点对应着一个fail,倒过来的话那么实际上这就是一棵树,即fail树。

这样再来看我们刚刚的问题,y对x有贡献,那么在fail树上,y或 原rie树上y的某个祖先(即这个单词的每一个字母) 肯定在x的某棵子树(fail树)上。

那么我们只要在原trie树上DFS,依次标记每一个字母赋值1,到一个y的结尾,对于每一个要求的x,就相当于在fail树上求x的子树和。

实现的话,可以在fail树dfs序用树状数组做就好了。。

 

#include<bits/stdc++.h>
#define RG register
#define IL inline
#define LL long long
#define lowbit(x) x&(-x)
using namespace std;

IL int gi () {
    int x=0; char ch=0;
    while (ch<'0'||ch>'9') ch=getchar();
    while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return x;
}

const int N=2e5+10;

queue <int> q;
char str[N],ss[N];
int tot,head[N];
int Time,low[N],dfn[N],ql[N],qr[N],BIT[N];
int n,num,wd[N],fa[N],f[N],tr[N][27],Tr[N][27],End[N],ans[N];

struct Query {int id,x,y;}qur[N];
IL bool cmp (Query A,Query B) {return A.y<B.y;}

struct Edge {int next,to;}e[N];
IL void make (int from,int to) {e[++tot]=(Edge){head[from],to};head[from]=tot;}
IL void Get_tree () {for (RG int i=1;i<=num;++i) make(f[i],i);}

IL void modify (int x,int v) {for(;x<=Time;x+=lowbit(x)) BIT[x]+=v;}
IL int query (int x) {RG int ans=0;for(;x;x-=lowbit(x)) ans+=BIT[x];return ans;}

IL void Get_Fail () {
    RG int i,x,now;
    for (i=1;i<=26;++i)
        if (tr[0][i]) q.push(tr[0][i]);
    while (!q.empty()) {
        x=q.front(),q.pop();
        for (i=1;i<=26;++i) {
            now=tr[x][i];
            if (!now) tr[x][i]=tr[f[x]][i];
            else f[now]=tr[f[x]][i],q.push(now);
        }
    }
}

void dfs (int x) {
    RG int i;
    dfn[x]=++Time;
    for (i=head[x];i;i=e[i].next) dfs(e[i].to);
    low[x]=Time;
}

void DFS (int x) {
    RG int i;
    modify(dfn[x],1);
    if (End[x])
        for (i=ql[End[x]];i<=qr[End[x]];++i) 
            ans[qur[i].id]=query(low[wd[qur[i].x]])-query(dfn[wd[qur[i].x]]-1);
    for (i=1;i<=26;++i)
        if (Tr[x][i]) DFS(Tr[x][i]);
    modify(dfn[x],-1);
}

int main ()
{
    freopen ("dat.in","r",stdin);
    freopen ("dat.out","w",stdout);
    RG int i,j,p=0,x,y,len,id,now;
    scanf("%s",str),n=gi(),len=strlen(str);
    for (i=1;i<=n;++i) x=gi(),y=gi(),qur[i]=(Query){i,x,y};
    sort(qur+1,qur+n+1,cmp);
    for (i=1,j=1;i<=n;++i) {
        ql[qur[i].y]=i;
        while (qur[j].y==qur[i].y) ++j;
        qr[qur[i].y]=j-1,i=j-1;
    }
    for (i=0,j=0,id=0;i<len;++i) {
        if (str[i]=='P') End[p]=++id,wd[id]=p;
        else if (str[i]=='B') p=fa[p];
        else {
            now=str[i]-'a'+1;
            if (!tr[p][now]) tr[p][now]=++num,fa[num]=p;
            p=tr[p][now];
        }
    }
    for (i=0;i<=num;++i)
        for (j=1;j<=26;++j) Tr[i][j]=tr[i][j];
    Get_Fail(),Get_tree(),dfs(0),DFS(0);
    for (i=1;i<=n;++i) printf("%d\n",ans[i]);
    return 0;
}
BY BHLLX

 

四、LuoguP3966单词

和上一题基本一样,只是要注意可能会有重复单词。。

 

#include<bits/stdc++.h>
#define RG register
#define IL inline
#define LL long long
#define lowbit(x) x&(-x)
using namespace std;

const int N=1e6+10;

queue <int> q;
char s[N];
int n,ans[N],Ed[N],wd[N],BIT[N];
int Time,dfn[N],low[N];
int tot,head[N],To[N],next[N];
int num,tr[N][27],Tr[N][27],F[N];

IL void make(int fr,int to) {next[++tot]=head[fr],To[tot]=to,head[fr]=tot;}
IL void Get_Edge() {for(RG int i=1;i<=num;++i) make(F[i],i);}

IL void modify(int x,int v) {for(;x<=Time;x+=lowbit(x)) BIT[x]+=v;}
IL int query(int x) {RG int sum=0;for(;x;x-=lowbit(x)) sum+=BIT[x];return sum;}

IL void Insert(char *s,int id) {
    int i,p=0,now,len=strlen(s);
    for (i=0;i<len;++i) {
        now=s[i]-'a'+1;
        if (!tr[p][now]) tr[p][now]=++num;
        p=tr[p][now];
    }
    ++Ed[p],wd[id]=p;
}

IL void Get_Fail() {
    RG int i,x,now;
    for (i=1;i<=26;++i)
        if (tr[0][i]) q.push(tr[0][i]);
    while (!q.empty()) {
        x=q.front(),q.pop();
        for (i=1;i<=26;++i) {
            now=tr[x][i];
            if (!now) tr[x][i]=tr[F[x]][i];
            else F[tr[x][i]]=tr[F[x]][i],q.push(now);
        }
    }
}

void dfs (int x) {
    dfn[x]=++Time;
    for (RG int i=head[x];i;i=next[i]) dfs(To[i]);
    low[x]=++Time;
}

void DFS (int x) {
    RG int i;
    modify(dfn[x],1);
    if (Ed[x]) {
        for (i=1;i<=n;++i)
            ans[i]+=(query(low[wd[i]])-query(dfn[wd[i]]-1))*Ed[x];
    }
    for (i=1;i<=26;++i)
        if (Tr[x][i]) DFS(Tr[x][i]);
    modify(dfn[x],-1);
}
 
int main ()
{
    RG int i,j;
    scanf("%d",&n);
    for (i=1;i<=n;++i)
        scanf("%s",s),Insert(s,i);
    for (i=0;i<=num;++i)
        for (j=1;j<=26;++j) Tr[i][j]=tr[i][j];
    Get_Fail(),Get_Edge(),dfs(0),DFS(0);
    for (i=1;i<=n;++i) printf("%d\n",ans[i]);
    return 0;
}
BY BHLLX

 

 五、LuoguP3966最短母串问题

其实这一是一道很好的DP题,这里用某神犇的AC自动机+最短路解决。

我们先对所有的串建一个AC自动机。

若一个串x的后缀 和另一个串y的前缀 可以重合一部分,那么在Trie图上x可以直接连向y的重合部分以下的那个节点。

所以我们直接在这个Trie图上跑一个 从根节点出发的 经过所有单词末尾节点的 最短路,用SPFA,因为可能会有环。

具体实现的话,我们相当与就在原来的基础上多了一维状态S,S是表示经过的末尾节点的集合。

至于方案的输出,我们直接DFS,详见代码。

注意:

① 空间只有32MB,还是得注意一下。

② 再跑最短路之前,我们需要提前把一个节点i所有的Fail祖先的结尾状态继承一下,否则有可能会造成漏掉那些本身就是其他串的子串的串。

 例如:HNOI,NOI,NOIP,IOI。 Ans:HNOIPIOI。可以用来检验一下。

 

#include<bits/stdc++.h>
#define RG register
#define IL inline
#define Mp make_pair
using namespace std;

IL int gi () {
    RG int x=0,w=0; char ch=0;
    while (ch<'0'||ch>'9') {if (ch=='-') w=1;ch=getchar();}
    while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return w?-x:x;
}

const int N=13;
const int M=605;
const int INF=0x3f3f3f3f;

queue <int> q;
queue < pair<int,int> > que;
string ss;
char str[N][51];
int num,tr[M][27],F[M];
int n,All,ans=INF,Inq[M][1<<N],Ed[M],f[M][1<<N];

IL void Insert(char *s,int id) {
    int i,p=0,len=strlen(s),now;
    for (i=0;i<len;++i) {
        now=s[i]-'A'+1;
        if (!tr[p][now]) tr[p][now]=++num;
        p=tr[p][now];
    }
    Ed[p]|=(1<<id-1);
}

IL void Get_Fail() {
    RG int i,x,now;
    for (i=1;i<=26;++i)
        if (tr[0][i]) q.push(tr[0][i]);
    while (!q.empty()) {
        x=q.front(),q.pop();
        for (i=1;i<=26;++i) {
            now=tr[x][i];
            if (!now) tr[x][i]=tr[F[x]][i];
            else F[now]=tr[F[x]][i],q.push(now);
        }
    }
}

IL void SPFA() {
    RG int i,j,x,s,y,now;
    for (i=1;i<=num;++i)
        for (j=i;j;j=F[j]) Ed[i]|=Ed[j];
    memset(f,0x3f,sizeof(f));
    f[0][0]=0;
    Inq[0][0]=1,que.push(Mp(0,0));
    while (!que.empty()) {
        x=que.front().first,s=que.front().second,que.pop(),Inq[x][s]=0;
        for (i=1;i<=26;++i) {
            if(!(y=tr[x][i])) continue;
            now=s|Ed[y];
            if (f[x][s]+1<f[y][now]) {
                f[y][now]=f[x][s]+1;
                if (!Inq[y][now]) que.push(Mp(y,now)),Inq[y][now]=1;
            }
        }
    }
    for (i=1;i<=num;++i) ans=min(ans,f[i][All]);
}

void DFS(int x,int state,string s)  {
    if (f[x][state]>ans) return;
    if (state==All&&f[x][state]==ans) {cout<<s<<endl;exit(0);}
    RG int i,y;
    for (i=1;i<=26;++i) 
        if ((y=tr[x][i])) {
            if (f[x][state]+1==f[y][state|Ed[y]]) DFS(y,state|Ed[y],s+(char)(i+'A'-1));
        }
}

int main ()
{
    RG int i;
    n=gi(),All=(1<<n)-1;
    for (i=1;i<=n;++i)
        scanf("%s",str[i]),Insert(str[i],i);    
    Get_Fail(),SPFA(),DFS(0,0,ss);    
    return 0;
}
BY BHLLX

 

posted @ 2019-01-28 14:01  薄荷凉了夏  阅读(158)  评论(0编辑  收藏  举报