AC自动机详解

首先,AC自动机,不是 自动Accepted机,这是一个多模字符串匹配算法,学习这个算法,首先要熟悉kmp算法这个单模字符串匹配算法,然后,我们知道有个高效的多模字符串匹配算法,叫字典树,它处理的是一些单词在一个句子里出现了几次,但假如不是在一个句子里,而是在一个字符串里呢?那它就显得很弱了,所以,AC自动机出现了!

 

Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法

                                                                                                                                                        ——引自百度百科

 

先讲一下AC自动机的工作流程吧:

1、以所有模式串构建一棵trie树(字典树)

2、构建每个节点的fail指针

3、与文本串进行匹配

 

第一个流程就不用讲了吧,就是字典树的模板,重点讲的是第二个流程——fail指针的构建,每个节点的fail指针,就好比kmp里面模式串的next数组,当这个点失配时,应该跳到哪个点继续匹配,不一定是跳到这一个单词的前面,有可能会跳到别的单词上,某个节点的fail指针构建的根据是:root到这个节点的路径的所有后缀中,刚好是某个单词的前缀的后缀中的最长的那个,这样说可能比较抽象,来看看下面这幅图吧:

这是构建8号节点的fail指针时的样子,可以发现,1到8的路径的后缀分别是:cab、ab、b  ,其中只有ab这个后缀刚好是某个单词的前缀,也就是ab这个单词,于是,8的fail指针就指向3

那么fail指针怎么求呢?可以类比一下kmp的next数组是怎么求的,在kmp算法中,next[i]是由next[i-1]得到的,那么同样的,某个节点的fail指针也可以由它的父亲节点得到,在kmp里,我们使一个j=next[i-1],当s[j+1]!=s[i]时,j=next[j],直到j=-1或s[j+1]=s[i],同样的,在AC自动机里面,我们还是让j=tree[ree[i].fa].fail,并且加一个c,让c=i和i的father之间的边上的字符,然后如果j没有c这个儿子,j=tree[j].fail,直到j=-1或j有c这个儿子了,就让i的fail指向j的c这个儿子,因为每一个节点的fail都可能由层次比他浅的节点得到,所以我们需要用广度优先搜索,来遍历每个点,如果还不理解,可以结合kmp算法来想一下就好了

代码如下:

void makefail()//注意要使tree[0].fail=-1,和kmp里面让next[0]=-1是一个道理 
{
	//队列q有三个属性,x表示当前点在树中的位置,fa就是它的父亲,c表示这个节点与它的父亲之间那条边上的字母 
    for(int i=1;i<=26;i++)//先将root的儿子都加进队列,我将a~z转成1~26来存 
    if(tree[0].son[i]!=-1)q[ed].x=tree[0].son[i],q[ed].fa=0,q[ed++].c=i;
    while(st!=ed)
    {
        int j=tree[q[st].fa].fail;//j先表示父亲的fail 
        while(j!=-1&&tree[j].son[q[st].c]==-1)j=tree[j].fail;//如果还没到root并且j没有这个儿子就继续往前跳 
        if(j!=-1)tree[q[st].x].fail=tree[j].son[q[st].c];//假如没走到根,那么就使它的fail指向j的儿子这个点 
        else tree[q[st].x].fail=0;//否则只能指向根 
        for(int i=1;i<=26;i++)//把它的儿子加入队列 
        if(tree[q[st].x].son[i]!=-1)q[ed].x=tree[q[st].x].son[i],q[ed].fa=q[st].x,q[ed++].c=i,ed=ed>1000000?1:ed;
        st++;st=st>1000000?1:st;//循环队列 
    }
}

那么匹配起来就很容易了,我们以洛谷上的一道模板题作为例子,那么我们建树代码如下:

void build(char ss[],int z)
{
    int now=0; 
    for(int i=0;i<z;i++)//这里和字典树的建树一样 
    {
        if(tree[now].son[ch(ss[i])]!=-1)now=tree[now].son[ch(ss[i])];
        else
        {
            len++;
            tree[now].son[ch(ss[i])]=len;
            now=len;
        }
    }
    tree[now].end++;//记录以这个节点结尾的单词数量 
    return;
}

匹配代码:

int i=1,j=0,ans=0;
int m=strlen(wbc+1);//wbc就是文本串
memset(v,false,sizeof(v));//优化,如果来过这个节点就不用再来了,这个是在循环求解的时候才用到的 
while(i<=m)
{
    if(tree[j].son[ch(wbc[i])]==-1)//假如匹配不成功
    {
        if(j==0)i++;//假如j==0,说明在root这里就匹配失败了,那只能把文本串的指针往后移一位
        else j=tree[j].fail;//否则就跳到fail上
    }
    else//如果匹配成功
    {
        j=tree[j].son[ch(wbc[i])];//往下走
        int pp=j;
        while(pp!=-1&&!v[pp])ans+=tree[pp].end,tree[pp].end=0,v[pp]=true,pp=tree[pp].fail;
//循环求解,如果匹配成功到了字典树上的j节点,那么j节点的fail指针指向的节点也一定匹配成功了
        i++;//往后移一位
    }
    if(ans==n)break;//小优化,已经找到全部单词就退出
}

完整的代码:

#include <cstdio>
#include <cstring>

struct node{
    int end,son[27],fail;
    node()
    {
        end=fail=0;
        memset(son,-1,sizeof(son));
    }
}tree[1000010];
int n,len=0;
char s[1000010];
bool v[1000010];
int ch(char x){return x-'a'+1;}
void build(char ss[],int z)
{
    int now=0; 
    for(int i=0;i<z;i++)
    {
        if(tree[now].son[ch(ss[i])]!=-1)now=tree[now].son[ch(ss[i])];
        else
        {
            len++;
            tree[now].son[ch(ss[i])]=len;
            now=len;
        }
    }
    tree[now].end++;
    return;
}
struct nod{int x,fa,c;};
nod q[1000010];
int st=1,ed=1;
void makefail()
{
    for(int i=1;i<=26;i++)
    if(tree[0].son[i]!=-1)q[ed].x=tree[0].son[i],q[ed].fa=0,q[ed++].c=i;
    while(st!=ed)
    {
        int j=tree[q[st].fa].fail;
        while(j!=-1&&tree[j].son[q[st].c]==-1)j=tree[j].fail;
        if(j!=-1)tree[q[st].x].fail=tree[j].son[q[st].c];
        for(int i=1;i<=26;i++)
        if(tree[q[st].x].son[i]!=-1)q[ed].x=tree[q[st].x].son[i],q[ed].fa=q[st].x,q[ed++].c=i,ed=ed>1000000?1:ed;
        st++;st=st>1000000?1:st;
    }
}
char wbc[1000010];

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%s",s);
        build(s,strlen(s));
    }
    tree[0].fail=-1;
    makefail();
    int i=1,j=0,ans=0;
    scanf("%s",wbc+1);
    int m=strlen(wbc+1);
    memset(v,false,sizeof(v));
    while(i<=m)
    {
        if(tree[j].son[ch(wbc[i])]==-1)
        {
            if(j==0)i++;
            else j=tree[j].fail;
        }
        else
        {
            j=tree[j].son[ch(wbc[i])];
            int pp=j;
            while(pp!=-1&&!v[pp])ans+=tree[pp].end,tree[pp].end=0,v[pp]=true,pp=tree[pp].fail;
            i++;
        }
        if(ans==n)break;
    }
    printf("%d",ans);
}

update on 2020.6.17:写了个更顺眼的版本:

#include <cstdio>
#include <cstring>
#include <ctime>
#define maxn 1000010

int n; char s[maxn];
struct node{
	int end,mat;node *fail,*fa,*next[26];
	node(node *FA):end(0),mat(0),fail(NULL),fa(FA){for(int i=0;i<26;i++)next[i]=NULL;}
}*root=new node(NULL);
void ins(int len)
{
	node *now=root;
	for(int i=0;i<len;i++)
	if(now->next[s[i]-'a'])now=now->next[s[i]-'a'];
	else now=now->next[s[i]-'a']=new node(now);
	now->end++;
}
node *q[maxn];int st=1,ed=0;
void makefail()
{
	for(int i=0;i<26;i++)if(root->next[i])
	q[++ed]=root->next[i],q[ed]->fail=root;
	while(st<=ed)
	{
		node *now=q[st++],*j;
		for(int i=0;i<26;i++)if(now->next[i])
		{
			j=now->fail;while(j&&!j->next[i])j=j->fail;
			if(j)now->next[i]->fail=j->next[i];
			else now->next[i]->fail=root;
			q[++ed]=now->next[i];
		}
	}
}

int main()
{
	scanf("%d",&n); for(int i=1,len;i<=n;i++)
	scanf("%s",s),len=strlen(s),ins(len); makefail();
	scanf("%s",s);n=strlen(s);
	node *now=root;
	for(int i=0;i<n;)
	{
		if(!now->next[s[i]-'a']){
			if(now!=root)now=now->fail;
			else i++;
		}
		else now=now->next[s[i++]-'a'],now->mat=1;
	}
	int ans=0;for(int i=ed;i>=1;i--)
	q[i]->fail->mat|=q[i]->mat,ans+=q[i]->mat*q[i]->end;
	printf("%d",ans);
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值