首页 > 技术文章 > AC自动机

kylehz 2015-04-24 00:40 原文

学习ac自动机需要先会kmp和trie:kmp | trie

终于入门了,看了一天,现在整理一下。

算法简介:

AC自动机主要用于解决多模式串的匹配问题。

如hdu2222:

给定N(N <= 10000)个长度不大于50的模式串,再给定一个长度为L(L <= 106)目标串,求目标串出现了多少个模式串。

模式串: she he say shr her  目标串:yasherhs  目标串中出现的模式串一共有3个,分别为 she he her

算法步骤:

1.先用模式串建一个字典树

2.对字典序中的节点构建失配指针,构成trie图

3.用目标串进行查询

 

构建字典树

Trie 节点的结构体

struct Trie
{
    Trie *next[maxn];
    Trie *fail;
    int cnt;//结尾标记,统计以该节点结尾的单词个数
    int v;//前缀标记,统计以该节点前的串为前缀的单词个数
    Trie()
    {
        fail=NULL;
        cnt=0;
        v=0;
        for(int i=0;i<maxn;i++)
            next[i]=NULL;
    }
}

 

Trie的构建不多说了,详见字典树

void creatTrie(Trie *root,char *str)
{
    Trie *p=root;
    p=root;
    for(int i=0;str[i];i++)
    {
        int id=charToNum(str[i]);
        if(id==-1)
        {
            puts("error");
            return;
        }
        if(p->next[id]==NULL)
            p->next[id]=new Trie();
        p=p->next[id];
        p->v++;
    }
    p->cnt++;
}

失配指针

失配指针的含义

一个模式串的某个字符匹配失败的时候,就跳到它的失败指针上继续匹配,重复上述操作,直到这个字符匹配成功,所以失败指针一定满足一个性质,它指向的一定是某个串的前缀,并且这个前缀是当前结点所在前缀的后缀,而且一定是最长后缀。仔细理解一下这句话,首先,一定是某个串的前缀,这是显然的,因为trie树本来就是前缀树,它的任意一个结点都是某个模式串的前缀;然后再来看后面一句话,为了让当前字符能够找到匹配,那么当前结点的某个后缀必须要和某个模式串的前缀相匹配,这个性质就和KMP的next数组不谋而合了。

如何利用BFS求出所有结点的失败指针

  1) 对于根结点root的失败指针,我们将它直接指向NULL,对于根结点下所有的子结点,失败指针一定是指向root的,因为当一个字符都不能匹配的时候,自然也就不存在更短的能够与之匹配的前缀了;

  2) 将求完失败指针的结点插入队列中;

  3) 每次弹出一个结点now,询问它的每个字符对应的子结点,为了阐述方便,我们将now的i号子结点记为now->next[i]:

         a) 如果now->next[i]为NULL,那么将now->next[i]指向now的失败指针的i号子结点, 即 now->next[i] = now->fail->next[i];

         b) 如果now->next[i]不等于NULL,则需要构造now->next[i]的失败指针,由于a)的操作,我们知道now的失败指针一定存在一个i号子结点,即now->fail->next[i],那么我们将now->next[i]的失败指针指向它,即now->next[i]->fail = now->fail->next[i];

  4) 重复2)的操作直到队列为空;

void build_ac(Trie *root)
{
    Trie *p,*tmp;
    int head=0,tail=0;
    q[tail++]=root;
    while(head!=tail)
    {
        p=q[head++];
        for(int i=0;i<maxn;i++)
        {
            tmp=(p==root)?root:p->fail->next[i];
            if(p->next[i]==NULL){
                p->next[i]=tmp;
            }
            else{
                p->next[i]->fail=tmp;
                q[tail++]=p->next[i];
            }
        }
    }
}

目标串匹配

对目标串进行匹配的时候,同样需要扫描目标字符串。由于trie图已经创建完毕,每个结点读入一个字符的时候都能够进入到下一个状态,所以我们只需要根据目标串给定的字符进行遍历,然后每次检查当前的结点是否是结尾结点,当然还需要检查p的失败指针指向的结点...累加所有的cnt和即为模式串的个数。

int query_ac(Trie *root,char *str)
{
    int cnt=0;
    Trie *p=root,*tmp=NULL;
    for(int i=0;str[i];i++)
    {
        int id=charToNum(str[i]);
        if(id==-1)
        {
            p=root;
            continue;
        }
        p=p->next[id];
        tmp=p;
        while(tmp!=root&&tmp->cnt!=-1)
        {
            if(tmp->cnt)
            {
                cnt+=tmp->cnt;
                tmp->cnt=-1;
            }
            tmp=tmp->fail;
        }
    }
    return cnt;
}

 

hdu2222

1.构建字典树

2.构造失败指针

用BFS来构造失败指针,与KMP算法相似的思想。

首先,root入队,第1次循环时处理与root相连的字符,也就是各个单词的第一个字符h和s,因为第一个字符不匹配需要重新匹配,所以第一个字符都指向root(root是Trie入口,没有实际含义)失败指针的指向对应下图中的(1),(2)两条虚线;

第2次进入循环后,从队列中先弹出h,接下来p指向h节点的fail指针指向的节点,也就是root;p=p->next[i], 则把节点e的fail指针指向root表示没有匹配序列,对应图-2中的(3),然后节点e进入队列;

第3次循环时,弹出的第一个节点a的操作与上一步操作的节点e相同,把a的fail指针指向root,对应图-2中的(4),并入队;

第4次进入循环时,弹出节点h(图中左边那个),这时操作略有不同。由于p->next[i]!=NULL(root有h这个儿子节点,图中右边那个),这样便把左边那个h节点的失败指针指向右边那个root的儿子节点h,对应图-2中的(5),然后h入队。以此类推:在循环结束后,所有的失败指针就是图-2中的这种形式。

 

模式串: she he say shr her  目标串:yasherhs  目标串中出现的模式串一共有3个,分别为 she he her

3.扫描

构造好Trie和失败指针后,我们就可以对主串进行扫描了。这个过程和KMP算法很类似,但是也有一定的区别,主要是因为AC自动机处理的是多串模式,需要防止遗漏某个单词,所以引入temp指针。

匹配过程分两种情况:(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。

 对照上图,看一下模式匹配这个详细的流程,其中模式串为yasherhs。对于i=0,1。Trie中没有对应的路径,故不做任何操作;i=2,3,4时,指针p走到左下节点e。因为节点e的count信息为1,所以cnt+1,并且讲节点e的count值设置为-1,表示改单词已经出现过了,防止重复计数,最后temp指向e节点的失败指针所指向的节点继续查找,以此类推,最后temp指向root,退出while循环,这个过程中count增加了2。表示找到了2个单词she和he。当i=5时,程序进入第5行,p指向其失败指针的节点,也就是右边那个e节点,随后在第6行指向r节点,r节点的count值为1,从而count+1,循环直到temp指向root为止。最后i=6,7时,找不到任何匹配,匹配过程结束。

 

注意:AC自动机建立的过程改变了字典序原有的next[],所以不能再用字典序中的方法查询和释放内存

动态写法:

#include<cstdio>
#include<cstring>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
#define maxn 26
#define N 500010 //定义队列大小
struct Trie
{
    Trie *next[maxn];
    Trie *fail;
    int cnt;//结尾标记,统计以该节点结尾的单词个数
    int v;//前缀标记,统计以该节点前的串为前缀的单词个数
    Trie()
    {
        fail=NULL;
        cnt=0;
        v=0;
        for(int i=0;i<maxn;i++)
            next[i]=NULL;
    }
}*q[N];
int charToNum(char s)
{
    if(s>='a'&&s<='z')return s-'a';
    return -1;
}
void creatTrie(Trie *root,char *str)
{
    Trie *p=root;
    p=root;
    for(int i=0;str[i];i++)
    {
        int id=charToNum(str[i]);
        if(id==-1)
        {
            puts("error");
            return;
        }
        if(p->next[id]==NULL)
            p->next[id]=new Trie();
        p=p->next[id];
        p->v++;
    }
    p->cnt++;
}
void build_ac(Trie *root)
{
    Trie *p,*tmp;
    int head=0,tail=0;
    q[tail++]=root;
    while(head!=tail)
    {
        p=q[head++];
        for(int i=0;i<maxn;i++)
        {
            tmp=(p==root)?root:p->fail->next[i];
            if(p->next[i]==NULL){
                p->next[i]=tmp;
            }
            else{
                p->next[i]->fail=tmp;
                q[tail++]=p->next[i];
            }
        }
    }
}
int query_ac(Trie *root,char *str)
{
    int cnt=0;
    Trie *p=root,*tmp=NULL;
    for(int i=0;str[i];i++)
    {
        int id=charToNum(str[i]);
        if(id==-1)
        {
            p=root;
            continue;
        }
        p=p->next[id];
        tmp=p;
        while(tmp!=root&&tmp->cnt!=-1)
        {
            if(tmp->cnt)
            {
                cnt+=tmp->cnt;
                tmp->cnt=-1;
            }
            tmp=tmp->fail;
        }
    }
    return cnt;
}
char qu[1000010];
int main()
{
    int T,n;
    scanf("%d",&T);
    char str[50];
    while(T--)
    {
        Trie *root=new Trie();
        scanf("%d",&n);
        while(n--)
        {
            scanf("%s",&str);
            creatTrie(root,str);
        }
        build_ac(root);
        scanf("%s",qu);
        printf("%d\n",query_ac(root,qu));
      //  Delete(root); AC自动机不能这么释放内存
    }
    return 0;
}
View Code 

静态写法:

注意姿势:

query里面vis的作用 可以大大降低时间复杂度,这一步很关键。 是600+ms 优化到200+ms的关键

#include<cstdio>
#include<cstring>
#include<string>
#include<queue>
#include<iostream>
#include<algorithm>
using namespace std;
#define maxn 26
#define N 500050 //定义队列大小,单词个数*长度
struct Trie
{
    int next[N][maxn];
    int fail[N];//失配指针
    int cnt[N];//结尾标记,统计以该节点结尾的单词个数
    int v[N];//前缀标记,统计以该节点前的串为前缀的单词个数
    int q[N];//队列缓存
    bool vis[N];
    int root,L;
    void inti()
    {
        L=0;
        root=newnode();
    }
    int newnode()
    {
        cnt[L]=0;
        v[L]=0;
        for(int i=0;i<maxn;i++)
        {
            next[L][i]=-1;//-1表示空节点
        }
        v[L]=0;
        cnt[L++]=0;
        return L-1;
    }
    int charToNum(char s)
    {
        if(s>='a'&&s<='z')return s-'a';
        return -1;
    }
    void insert(char str[])
    {
        int now=root;
        for(int i=0;str[i];i++)
        {
            int id=charToNum(str[i]);
            if(id==-1)
            {
                puts("error");
                return;
            }
            if(next[now][id]==-1)
                next[now][id]=newnode();
            now=next[now][id];
            v[now]++;
        }
        cnt[now]++;
    }
    void build()
    {
        int head=0,tail=0;
        int now,tmp;
        q[tail++]=root;
        while(head!=tail)
        {
            now=q[head++];
            for(int i=0;i<maxn;i++)
            {
                tmp=(now==root)?root:next[fail[now]][i];
                if(next[now][i]==-1)
                {
                    next[now][i]=tmp;
                }
                else
                {
                    fail[next[now][i]]=tmp;
                    q[tail++]=next[now][i];
                }

            }
        }
    }
    int query(char str[])
    {
        memset(vis,false,sizeof(vis));
        int ans=0;
        int now=root,tmp;
        for(int i=0;str[i];i++)
        {
            int id=charToNum(str[i]);
            if(id==-1)
            {
                now=root;
                continue;
            }
            now=next[now][id];
            tmp=now;
            while(tmp!=root)
            {
                if(vis[tmp])break;
                vis[tmp]=true;
                ans+=cnt[tmp];
                tmp=fail[tmp];
            }
        }
        return ans;
    }
};
char qu[1000010];
Trie ac;
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        int n;
        scanf("%d",&n);
        char str[100];
        ac.inti();
        while(n--)
        {
            scanf("%s",str);
            ac.insert(str);
        }
        ac.build();
        scanf("%s",qu);
        printf("%d\n",ac.query(qu));
    }
    return 0;
}
View Code

 

 

 

参考:

zoj 3545 AC自动机+状态dp http://blog.csdn.net/Guard_Mine/article/details/45151769

http://www.cppblog.com/menjitianya/archive/2014/07/10/207604.html

http://blog.csdn.net/liuqiyao_01/article/details/8798241

http://blog.csdn.net/niushuai666/article/details/7002823

http://blog.csdn.net/niushuai666/article/details/7002736

 

推荐阅读