AC自动机步骤详解

AC自动机


算法意义:

求多个字符串是否在主串中出现过。可依据情况分别求出出现次数,出现位置等。

学习基础:

要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。

AC自动机的构造:

1.构造一棵Trie,作为AC自动机的搜索数据结构。
2.构造fail指针,使当前字符失配时跳转到具有最长公共前后缀的字符继续匹配。如同 KMP算法一样, AC自动机在匹配时如果当前字符匹配失败,那么利用fail指针进行跳转。由此可知如果跳转,跳转后的串的前缀,必为跳转前的模式串的后缀并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点。所以我们可以利用 bfs在 Trie上面进行 fail指针的求解。
3.扫描主串进行匹配。

AC自动机详讲:

我们给出5个单词,say,she,shr,he,her。给定字符串为yasherhs。问多少个单词在字符串中出现过?

一、构建trie树

AC自动机步骤详解_第1张图片
首先这个trie树的每个节点必须包含以下元素:
1.一个失败指针,记录匹配失败后将要跳转的节点。 代码中的node *fail;
2.26个儿子节点,记录的是26个儿子,编号代表下一个字母的值。  代码中的node *child[26];
3.一个标记当前是不是一个单词的结尾。代码中的int count;
4.顺便调用构造方法给每个节点赋初值。
节点代码:

struct node
{
    node *fail;
    node *child[26];
    int count;
    node()
    {
        fail=NULL;
        count =0;
        memset(child,NULL,sizeof(child));
    }
};


然后开始建树。trie树教程点此。
建树代码:

void insert(node *t,char *p)
{
    int index;
    while(*p!='\0')
    {
        index=*p-'a';
        if(t->child[index]==NULL)
            t->child[index]=new node();
        t=t->child[index];
        p++;
    }
    t->count=1;
}


二、构建失败指针

AC自动机步骤详解_第2张图片
root的失败指针为NULL。
求某个节点的失败指针的方法:
1.判断他的父亲节点是不是root。
2.若是,他的失败指针指向的是root。
3.若不是,找到父亲节点失败指针所指的节点的子节点是否含有和所求节点相同的字母。例如,上图中求she中e的失败指针:e的父亲节点h的失败指针是her中的h,而h的儿子节点有和e相同的节点。
4.如果含有,失败指针就是找到的那个和本节点相同的节点,如she中e的失败指针就是he中的e节点。
5.如果没有,找到他父亲的失败指针的失败指针继续3.。直到到root节点都没找到的话,就令本解点的失败指针为root。如shr的r节点,他的父亲节点h的失败指针是her中的h。其子节点中只有e,没有r所以再求her中h的失败指针,他是root,所以shr中r的失败指针是root。
构建失败指针部分代码如下:

void ac(node *t)
{
    t->fail=NULL;
    q.push(t);
    node *temp;
    node *tmp;
    int i;
    while(!q.empty())
    {
        temp=q.front();
        q.pop();
        for(i=0;i<26;i++)
        {
            if(temp->child[i]!=NULL)
            {
                if(temp==t)
                    temp->child[i]->fail=t;
                else
                {
                    tmp=temp->fail;
                    while(tmp!=NULL)
                    {
                        if(tmp->child[i]!=NULL)
                        {
                            temp->child[i]->fail=tmp->child[i];
                            break;
                        }
                        tmp=tmp->fail;
                    }
                    if(tmp==NULL)
                        temp->child[i]->fail=t;
                }
                q.push(temp->child[i]);
            }
        }
    }
}


三、扫描主串进行匹配。

构造好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时,找不到任何匹配,匹配过程结束。
代码如下:

int query(node *t,char *p)
{
    int index,cnt=0;
    node *tmp;
    while(*p!='\0')
    {
        index=*p-'a';
        while(t->child[index]==NULL &&t->fail!=NULL)
            t=t->fail;
        if(t->child[index]!=NULL)
        {
            t=t->child[index];
            tmp=t;
            while(tmp->count==1)
            {
                cnt++;
                tmp->count=-1;
                tmp=tmp->fail;
            }
        }
        p++;
    }
    return cnt;
}
void del(node *root)
{
    for(int i=0;i<26;i++)
        if(root->child[i]!=NULL)
            del(root->child[i]);
    delete root;
    root=NULL;
}

完整代码:

#include 
#include 
#include 

using namespace std;
struct node
{
    node *fail;
    node *child[26];
    int count;
    node()
    {
        fail=NULL;
        count =0;
        memset(child,NULL,sizeof(child));
    }
};
char s1[51];
char s2[1000001];
queue  q;

void insert(node *t,char *p)
{
    int index;
    while(*p!='\0')
    {
        index=*p-'a';
        if(t->child[index]==NULL)
            t->child[index]=new node();
        t=t->child[index];
        p++;
    }
    t->count=1;
}

void ac(node *t)
{
    t->fail=NULL;
    q.push(t);
    node *temp;
    node *tmp;
    int i;
    while(!q.empty())
    {
        temp=q.front();
        q.pop();
        for(i=0;i<26;i++)
        {
            if(temp->child[i]!=NULL)
            {
                if(temp==t)
                    temp->child[i]->fail=t;
                else
                {
                    tmp=temp->fail;
                    while(tmp!=NULL)
                    {
                        if(tmp->child[i]!=NULL)
                        {
                            temp->child[i]->fail=tmp->child[i];
                            break;
                        }
                        tmp=tmp->fail;
                    }
                    if(tmp==NULL)
                        temp->child[i]->fail=t;
                }
                q.push(temp->child[i]);
            }
        }
    }
}

int query(node *t,char *p)
{
    int index,cnt=0;
    node *tmp;
    while(*p!='\0')
    {
        index=*p-'a';
        while(t->child[index]==NULL &&t->fail!=NULL)
            t=t->fail;
        if(t->child[index]!=NULL)
        {
            t=t->child[index];
            tmp=t;
            while(tmp->count==1)
            {
                cnt++;
                tmp->count=-1;
                tmp=tmp->fail;
            }
        }
        p++;
    }
    return cnt;
}
void del(node *root)
{
    for(int i=0;i<26;i++)
        if(root->child[i]!=NULL)
            del(root->child[i]);
    delete root;
    root=NULL;
}
int main()
{
    int n,m;
    scanf("%d\n",&n);
    while(n--)
    {
        node *root=new node();
        scanf("%d\n",&m);
        while(m--)
        {
            gets(s1);
            insert(root,s1);
        }
        gets(s2);
        ac(root);
        printf("%d\n",query(root,s2));
        del(root);
    }
    return 0;
}



你可能感兴趣的:(数据结构与算法)