某模板题:链接:https://www.luogu.org/problemnew/show/P3796
P3796 【模板】AC自动机(加强版)
描述
有N个由小写字母组成的模式串以及一个文本串T。每个模式串可能会在文本串中出现多次。你需要找出哪些模式串在文本串T中出现的次数最多。
输入
输入含多组数据。
每组数据的第一行为一个正整数N,表示共有N个模式串,1≤N≤150。
接下去NN行,每行一个长度小于等于70的模式串。下一行是一个长度小于等于10^6 的文本串T。
输入结束标志为N=0
输出
对于每组数据,第一行输出模式串最多出现的次数,接下去若干行每行输出一个出现次数最多的模式串,按输入顺序排列。
样例输入
2
aba
bab
ababababac
6
beta
alpha
haha
delta
dede
tata
dedeltalphahahahototatalpha
0
样例输出
4
aba
2
alpha
haha
概论:
AC自动机是KMP算法(思想)与trie树(形式)的结合。KMP运用于单模匹配,失败后,将已完成匹配节点的后缀与模板串前缀匹配,然后将模板串前缀移至匹配失败的地方继续下一轮匹配。
而AC自动机则是多模匹配,给出多个模板串,可看作是一个字符串的集合,用trie树来作为字符串的集合,再在trie树上构建KMP失配函数,方法则是
- 构建trie树(将所有模板链构建成一个字典树)
- 构建fail指针(每次匹配失败后回到指针所指向的点,类似于KMP的next指针作用)
- 查询(利用字典树方法查询)
标准代码:
(某些大佬提供)
#include<cstdio>
#include<cstring>
#include<queue>
#include<iostream>
#define DEBUG printf("ASDASD");//dhy大佬教我的骚技能1
#define clear(x) memset(x,0,sizeof(x))//dhy大佬教我的骚技能2
using namespace std;
const int N=1e6;
int tree[N][27],ans[N]/*ans[i]表示第i号点是否为一个单词结尾,若是,储存值为该单词编号,否则为0*/,fail[N];
int cnt=0/*以0号节点为初始节点(用什么表示虚根?)*/,root;
void insert(char s[],int v)
{
int now=root;//now表示now号点
int len=strlen(s);
for(int i=0;i<len;++i)
{
int id=s[i]-'a';
if(!tree[now][id])
tree[now][id]=++cnt;
now=tree[now][id];
}
ans[now]=v;
return;
}
void getfail()
{
queue <int> q;
for(int i=0;i<26;++i)//单独处理(虚)根节点
{
if(tree[0][i])
{
q.push(tree[0][i]);
}
}
while(!q.empty())//从(虚)根的子节点开始处理,默认虚根子节点fail都指向虚根
{
int now=q.front();q.pop();
for(int i=0;i<26;i++)
{
int v=tree[now][i];
if(v)fail[v]=tree[fail[now]][i],q.push(v);
else tree[now][i]=tree[fail[now]][i];
}
}
}
int tmp[N];
void query(char s[])
{
int now=0,sum=0;
int len=strlen(s);
for(int i=0;i<len;++i)
{
now=tree[now][s[i]-'a'];
for(int j = now;j;j=fail[j])tmp[ans[j]]++;///
}
}
char T[200][200];//模板串
char S[N];
int main()
{
int n;
while(1){
scanf("%d",&n);
if(n==0)return 0;
clear(T),clear(S),clear(tmp),clear(tree),clear(ans),clear(fail);
cnt=0;
for(int i=1;i<=n;++i)
{
scanf("%s",T[i]);
insert(T[i],i);
}
getfail();
scanf("%s",S);
query(S);
int mx=0;
for(int i=1;i<=n;i++)
mx=max(mx,tmp[i]);
printf("%d\n",mx);
for(int i=1;i<=n;i++)
if(tmp[i]==mx)
printf("%s\n",T[i]);
}
return 0;
}
蒟蒻代码:
#include<cstdio>//问题:有模板串:ab与pabc
#include<cstring>
using namespace std;
const int N=1e6+5;
int n,ch[10505/*大小开错*/][30],nxt[N],cnt=1,que[10505],maxn,f[155],bo[10505],t=0;
struct fjy{
char st2[70];
int num;
}z[155];
char st1[N],st3[30],ans[155][75];
void insert(fjy x){//构建trie树
char s[30];
strcpy(s,x.st2);
int u=1,len=strlen(s);
for(int i=0;i<len;i++){
int c=s[i]-'a';
/* printf("%d ",c);*/
if(!ch[u][c])
ch[u][c]=++cnt;//printf("%d ",cnt);
u=ch[u][c];
}
bo[u]=x.num;//printf("%d %d",bo[u],cnt);/
return;
}
void bfs(){//构建fail指针
for(int i=0;i<26;i++)
ch[0][i]=1;
que[1]=1,nxt[1]=0;
for(int q1=1,q2=1;q1<=q2;q1++){
int u=que[q1];
for(int i=0;i<26;i++){
if(!ch[u][i])
{
ch[u][i]=ch[nxt[u]][i];//时间优化
/*if(ch[nxt[u]][i])
bo[nxt[u]]=bo[u];*/
}
else{
q2++;
que[q2]=ch[u][i];
int v=nxt[u];
nxt[ch[u][i]]=ch[v][i];//printf("%d\n",q1);
}
}
}
}
void find(char *s)
{
int u=1,c,k,len=strlen(s),ans1=0;
for(int i=0;i<len;i++){
c=s[i]-'a';
u=ch[u][c];//printf("%d\n",u);u的遍历无问题
//ans1+=bo[u];
/*if(bo[u]){///
f[bo[u]]++;u=nxt[u];
//printf("%d",f[1]);
}*/
for(int j=u;j;j=nxt[j]) f[bo[j]]++;//
}
return;
}
int main()
{
scanf("%d",&n);
while(n)
{
maxn=0;t=0;cnt=1;
memset(nxt,0,sizeof(nxt));
memset(f,0,sizeof(f));
memset(que,0,sizeof(que));
memset(bo,0,sizeof(bo));
memset(ch,0,sizeof(ch));
memset(st1,0,sizeof(st1));
memset(st3,0,sizeof(st3));
memset(ans,0,sizeof(ans));
for(int i=0;i<26;i++)
ch[0][i]=1,ch[1][i]=0;
for(int i=1;i<=n;i++)
{
scanf("%s",st3);
z[i].num=i,strcpy(z[i].st2,st3/*,sizeof(st3)*/);
insert(z[i]);
}
bfs();
scanf("%s",st1);
find(st1);
for(int i=1;i<=n;i++)
{
if(maxn<f[i]){
maxn=f[i];t=0;
memset(ans,0,sizeof(ans));
strcpy(ans[++t],z[i].st2);
}
else
if(maxn==f[i])
strcpy(ans[++t],z[i].st2);
}
printf("%d\n",maxn);
for(int i=1;i<=t;i++)printf("%s\n",ans[i]);
scanf("%d",&n);
}
return 0;
}
来自蒟蒻的挣扎:
#include<cstdio>//调试剂
#include<cstring>
using namespace std;
const int N=1e6+5;
int n,ch[10505][30],nxt[N],cnt=0,que[10505],maxn,f[155],bo[10505],t=0;
struct fjy{//模板串的相关信息
char st2[70];
int num;
}z[155];
char st1[N],st3[30],ans[155][75];
void insert(fjy x){//构建trie树
char s[30];
strcpy(s,x.st2);
int u=0,len=strlen(s);
for(int i=0;i<len;i++){
int c=s[i]-'a';
/* printf("%d ",c);*/
if(!ch[u][c])
ch[u][c]=++cnt;//printf("%d ",cnt);
u=ch[u][c];
}
bo[u]=x.num;//printf("%d %d",bo[u],cnt);/
return;
}
/*void bfs(){//构建fail指针
for(int i=0;i<26;i++)
ch[0][i]=1;
que[1]=1,nxt[1]=0;
for(int q1=1,q2=1;q1<=q2;q1++){
int u=que[q1];
for(int i=0;i<26;i++){
if(!ch[u][i])
ch[u][i]=ch[nxt[u]][i];//时间优化
else{
q2++;
que[q2]=ch[u][i];
int v=nxt[u];
nxt[ch[u][i]]=ch[v][i];//printf("%d\n",q1);
}
}
}
}*/
void bfs(){
int q1=1,q2=0;
for(int i=0;i<26;i++)
if(ch[0][i])
que[++q2]=ch[0][i];
for(q1,q2;q1<=q2;q1++){
int u=q1;
for(int i=0;i<26;i++){
int v=ch[u][i];
if(v){//存在目标子节点
nxt[v]=ch[nxt[u]][i];
que[++q2]=v;
}
else{//不存在目标子节点
ch[u][i]=ch[nxt[u]][i];
}
}
}
}
void find(char *s)
{
int u=0,c,k,len=strlen(s),ans1=0;
for(int i=0;i<len;i++){
c=s[i]-'a';
u=ch[u][c];//printf("%d\n",u);u的遍历无问题
//ans1+=bo[u];
/*if(bo[u]){///
f[bo[u]]++;u=nxt[u];
//printf("%d",f[1]);
}*/
for(int j=u;j;j=nxt[j]) f[bo[j]]++;//
}
return;
}
int main()
{
scanf("%d",&n);
while(n)
{
maxn=0;t=0;cnt=1;
memset(nxt,0,sizeof(nxt));
memset(f,0,sizeof(f));
memset(que,0,sizeof(que));
memset(bo,0,sizeof(bo));
memset(ch,0,sizeof(ch));
memset(st1,0,sizeof(st1));
memset(st3,0,sizeof(st3));
memset(ans,0,sizeof(ans));
for(int i=0;i<26;i++)
ch[0][i]=1,ch[1][i]=0;
for(int i=1;i<=n;i++)
{
scanf("%s",st3);
z[i].num=i,strcpy(z[i].st2,st3/*,sizeof(st3)*/);
insert(z[i]);
}
bfs();
scanf("%s",st1);
find(st1);
for(int i=1;i<=n;i++)
{
if(maxn<f[i]){
maxn=f[i];t=0;
memset(ans,0,sizeof(ans));
strcpy(ans[++t],z[i].st2);
}
else
if(maxn==f[i])
strcpy(ans[++t],z[i].st2);
}
printf("%d\n",maxn);
for(int i=1;i<=t;i++)printf("%s\n",ans[i]);
scanf("%d",&n);
}
return 0;
}
蒟蒻的逆袭:
#include<cstdio>//问题:有模板串:ab与pabc
#include<cstring>
using namespace std;
const int N=1e6+5;
int n,ch[10505/*大小开错*/][30],nxt[N],cnt=1,que[10505],maxn,f[155],bo[10505],t=0;
struct fjy{
char st2[70];
int num;
}z[155];
char st1[N],st3[75],ans[155][75];
void insert(fjy x){//构建trie树
char s[75];
strcpy(s,x.st2);
int u=1,len=strlen(s);
for(int i=0;i<len;i++){
int c=s[i]-'a';
/* printf("%d ",c);*/
if(!ch[u][c])
ch[u][c]=++cnt;//printf("%d ",cnt);
u=ch[u][c];
}
bo[u]=x.num;//printf("%d %d",bo[u],cnt);/
return;
}
void bfs(){//构建fail指针
for(int i=0;i<26;i++)
ch[0][i]=1;
que[1]=1,nxt[1]=0;
for(int q1=1,q2=1;q1<=q2;q1++){
int u=que[q1];
for(int i=0;i<26;i++){
if(!ch[u][i])
{
ch[u][i]=ch[nxt[u]][i];//时间优化
/*if(ch[nxt[u]][i])
bo[nxt[u]]=bo[u];*/
}
else{
q2++;
que[q2]=ch[u][i];
int v=nxt[u];
nxt[ch[u][i]]=ch[v][i];//printf("%d\n",q1);
}
}
}
}
void find(char *s)
{
int u=1,c,k,len=strlen(s),ans1=0;
for(int i=0;i<len;i++){
c=s[i]-'a';
u=ch[u][c];//printf("%d\n",u);u的遍历无问题
//ans1+=bo[u];
/*if(bo[u]){///
f[bo[u]]++;u=nxt[u];
//printf("%d",f[1]);
}*/
for(int j=u;j;j=nxt[j]) f[bo[j]]++;//
}
return;
}
int main()
{
scanf("%d",&n);
while(n)
{
maxn=0;t=0;cnt=1;
memset(nxt,0,sizeof(nxt));
memset(f,0,sizeof(f));
memset(que,0,sizeof(que));
memset(bo,0,sizeof(bo));
memset(ch,0,sizeof(ch));
memset(st1,0,sizeof(st1));
memset(st3,0,sizeof(st3));
memset(ans,0,sizeof(ans));
for(int i=0;i<26;i++)
ch[0][i]=1,ch[1][i]=0;
for(int i=1;i<=n;i++)
{
scanf("%s",st3);
z[i].num=i,strcpy(z[i].st2,st3/*,sizeof(st3)*/);
insert(z[i]);
}
bfs();
scanf("%s",st1);
find(st1);
for(int i=1;i<=n;i++)
{
if(maxn<f[i]){
maxn=f[i];t=0;
memset(ans,0,sizeof(ans));
strcpy(ans[++t],z[i].st2);
}
else
if(maxn==f[i])
strcpy(ans[++t],z[i].st2);
}
printf("%d\n",maxn);
for(int i=1;i<=t;i++)printf("%s\n",ans[i]);
scanf("%d",&n);
}
return 0;
}
错误总结:
- 虚根的建立:第一套蒟蒻程序中,以0节点为虚根,1节点为字典树的起始节点(然后就错了),第二套蒟蒻程序中,以0节点为虚根兼字典树的起始节点,对于起始节点的所有儿子单独处理,让所有儿子的fail指针都指向起始节点(然后就对了,至于为什么我也不知道)
- 数据的范围:第三套蒟蒻程序中,st2[]的范围改为了75,字典树建立当中的s[]的范围也改为了75
- 构建fail指针中,q1所指向的是队列中的点而不是点在字典树中的序号