ac自动机
ac自动机,就是在tire树的基础上,增加一个fail指针,表示当前结点匹配失败时,应该转移到哪个地方继续匹配(而“这个地方”也就是当前匹配成功的子串的最长后缀串的下一个结点),而不是每次都回到起点开始重新匹配。
如下图:我们在树上匹配“washer”时,当匹配到1号结点时,发现匹配不上后继结点‘r’,就转移到2号节点继续匹配;2号结点仍然匹配不上后继结点‘r’,就继续转移到3号结点。在这里fail[1]=2,fail[2]=3。处理fail数组时,fail[u]的值可以由u父亲的fail值得来。
查询
最后的查询操作中,每到树上一个结点,就遍历这个子串的所有后缀,通过储存权值信息的vs数组判断该这些后缀是否为Keyword。如上图中匹配到“ashe”时,就需要判断“she”“he”是否为Keyword。
//查找打印描述中包含多少关键字
//静态模板
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
const int manx=5e5+10;
const int max_len=1e6+10;
struct AC
{
int cou=0;
int tree[manx][27],fail[manx],vs[manx];
int newnode()
{
for(int i=0; i<26; i++)
tree[cou][i]=0;
vs[cou]=fail[cou++]=0;
return cou-1;
}
void init()
{
cou=0;
newnode();
}
//正常的字典树插入操作
void iinsert(char *s)
{
int len=strlen(s);
int u=0;
for(int i=0; i<len; i++)
{
int net=s[i]-'a';
if(!tree[u][net])
tree[u][net]=newnode();
u=tree[u][net];
}
vs[u]++;
}
//所有关键字插入字典树后,bfs遍历整棵树维护fail数组
void getfail()
{
int u=0;
queue<int>qu;
for(int i=0; i<26; i++)
if(tree[u][i])
qu.push(tree[u][i]);
while(!qu.empty())
{
u=qu.front();
qu.pop();
for(int i=0; i<26; i++)
{
if(tree[u][i])
{
fail[tree[u][i]]=tree[fail[u]][i];
qu.push(tree[u][i]);
}
else
tree[u][i]=tree[fail[u]][i];
//因为是宽搜,每次将当前结点的tree[u][0]~tree[u][25]维护好;
//之后就可以从父亲结点那里一步得到tree[u][i]的值:tree[fail[u]][i],从而省去了while循环找后继结点i的位置
//如上图中tree[2][r]就直接赋为4了
}
}
}
int query(char *s)
{
int u=0,ans=0;
int len=strlen(s);
for(int i=0;i<len;i++)
{
int net=s[i]-'a';
u=tree[u][net];
for(int j=u;j&&~vs[j];j=fail[j])//遍历所有后缀串,并在遍历后标记为-1,防止重复累加
{
ans+=vs[j];
vs[j]=-1;
}
}
return ans;
}
};
AC aut;
int t,n;
char str[max_len];
int main()
{
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
aut.init();
for(int i=0; i<n; i++)
{
scanf("%s",str);
aut.iinsert(str);
}
aut.getfail();
scanf("%s",str);
int ans=aut.query(str);
printf("%d\n",ans);
}
return 0;
}
动态写法
//动态写法时,要用delete释放空间
void eeraser(node* root)
{
for(int i=0;i<26;i++)
{
if(root->net[i]!=NULL)
eeraser(root->net[i]);
}
delete root;
}
//我是代码的搬运工( ̄▽ ̄)/(https://paste.ubuntu.com/p/T2YnF3VNVb/)
//下面fail的数组的处理和上面有些不一样:另外用las数组记录了u->net[i]处,的最长后缀所在的位置(vs有效的前提下)。
//然后查询的时候fail找到它后缀中第一个vs有效位置,最后遍历p->las统计所有有效后缀串
//静态的写法里面处理fail时也可以加上,vs有效的前提,但要另外开一个数组记录了
#include<iostream>
#include<string.h>
#include<algorithm>
#include<stdio.h>
#include<math.h>
#include<stdlib.h>
#include<queue>
using namespace std;
const int N = 26;
const int manx= 500005;
struct node
{
node* fail;
node* las;
node* net[N];
int vs;
node()
{
fail=NULL; las=NULL;
// memset(net,NULL,sizeof(net));
for(int i=0; i<26; i++) net[i] = NULL;
vs=0;
}
};
void iinsert(char* str,node* root)
{
int i=0;
while(str[i]>='a' && str[i] <= 'z')
{
int index=str[i]-'a';
if(root->net[index]==NULL)
root->net[index]=new node;
root=root->net[index];
i++;
}
root->vs++;
}
void getfail(node* root)
{
root->fail=NULL;
queue<node*>qu;
qu.push(root);
node* u;
while(!qu.empty())
{
u=qu.front();
qu.pop();
for(int i=0;i<26;i++)
{
if(u->net[i]!=NULL)
{
if(u == root)
u->net[i]->fail = root;
else
{
node *p = u->fail;
while(p != NULL)
{
if(p->net[i])
{
u->net[i]->fail = p->net[i];
break;
}
p = p->fail;
}
if(p == NULL) u->net[i]->fail = root;
if(u->net[i]->fail->vs) u->net[i]->las=u->net[i]->fail; /*****/
else u->net[i]->las=u->net[i]->fail->las;
}
qu.push(u->net[i]);
}
}
}
}
int query(char* str,node* root)
{
int len=strlen(str);
node* u=root;
int ans=0;
for(int i=0;i<len;i++)
{
int index=str[i]-'a';
while(u != root && !u->net[index]) u = u->fail;
u = u->net[index];
if(u == NULL) u = root;
node *p = u;
while(p != root && p != NULL) /*****/
{
if(p->vs!=-1)
{
ans += p->vs;
p->vs = -1;
}
else break;
p = p->las; /*****/
}
}
return ans;
}
void eeraser(node* root)
{
for(int i=0;i<26;i++)
{
if(root->net[i]!=NULL)
eeraser(root->net[i]);
}
delete root;
}
int main()
{
int t,n;
char str[100];
char str1[manx<<1];
scanf("%d",&t);
while(t--)
{
node* root=new node;
scanf("%d",&n);
while(n--)
{
scanf("%s",str);
iinsert(str,root);
}
getfail(root);
scanf("%s",str1);
printf("%d\n",query(str1,root));
eeraser(root);
}
}