AC自动机
时间有限,只过了板子和少数题目。。在应该搞DP的时候搞这个。。
关于AC自动机的介绍,LuoGu日报讲的挺好的,可以比较好的入门。
一、模板
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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; }
我的不知道为什么跑的贼慢。。。
硬是不会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树上跳来跳去的。。。(根据样例画图理解最佳)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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; }
刚了差不多一天。。。题是真的一道好题。。。
详细介绍可以看yyb神仙写的博客,写的很好。
这种题先打一个暴力,70分,还挺好想。。(然而我还是看了yyb神仙的代码调了一波)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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; }
上述代码有一个很耗时的问题,就是每次都重新插入,其实可以记录一下每个节点的父亲,就可以很快的继续执行插入。不然读入都会超时。
然后我们怎么考虑这个问题:
我们发现是求一个串x在另一个串y中出现了多少次。
反过来考虑的话就是分别求出:对于每一个y它能给对应的x的答案产生多少贡献
这个怎么求呢?我们要利用到fail(这个东西真的很神仙)。
一个节点对应着一个fail,倒过来的话那么实际上这就是一棵树,即fail树。
这样再来看我们刚刚的问题,y对x有贡献,那么在fail树上,y或 原rie树上y的某个祖先(即这个单词的每一个字母) 肯定在x的某棵子树(fail树)上。
那么我们只要在原trie树上DFS,依次标记每一个字母赋值1,到一个y的结尾,对于每一个要求的x,就相当于在fail树上求x的子树和。
实现的话,可以在fail树dfs序用树状数组做就好了。。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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; }
和上一题基本一样,只是要注意可能会有重复单词。。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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; }
其实这一是一道很好的DP题,这里用某神犇的AC自动机+最短路解决。
我们先对所有的串建一个AC自动机。
若一个串x的后缀 和另一个串y的前缀 可以重合一部分,那么在Trie图上x可以直接连向y的重合部分以下的那个节点。
所以我们直接在这个Trie图上跑一个 从根节点出发的 经过所有单词末尾节点的 最短路,用SPFA,因为可能会有环。
具体实现的话,我们相当与就在原来的基础上多了一维状态S,S是表示经过的末尾节点的集合。
至于方案的输出,我们直接DFS,详见代码。
注意:
① 空间只有32MB,还是得注意一下。
② 再跑最短路之前,我们需要提前把一个节点i所有的Fail祖先的结尾状态继承一下,否则有可能会造成漏掉那些本身就是其他串的子串的串。
例如:HNOI,NOI,NOIP,IOI。 Ans:HNOIPIOI。可以用来检验一下。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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; }