【AC自动机】AC自动机可以帮你自动AC吗

参考博文:AC自动机算法详解 (转载) (原文作者:DarkRaven,原文的链接失效了)
图片来源:AC自动机算法详解 (转载)


主要内容:

  • 什么是AC自动机
  • AC自动机用来做什么
  • 前置知识
  • 算法分析
  • 代码实现

什么是AC自动机

Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。要学会AC自动机,我们必须知道什么是Trie,也就是字典树。Trie树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
(来源:百度百科)

  • 所以AC自动机不是Accept automaton,是Aho-Corasick automaton,并不能帮你自动AC。

用来做什么?

AC自动机针对是多模式串匹配问题。例:给几个关键词(模式串),再给一篇文章,判断关键词是否在文章中出现,或出现的次数。

前置知识

  • 必须掌握:trie树
  • 建议掌握:KMP算法
    AC自动机的工作原理就是将模式串构建成一个trie树(字典树),然后对目标串进行匹配。其中构建trie树的关键在于,当失配(匹配失败)的时候,匹配转向失配节点的失败节点(或称转换节点)。构建trie树是AC自动机的关键,而失配的处理和kmp的处理思想相同,整体的算法思想也是将模式串先处理。所以建议掌握kmp算法,再进行AC自动机的学习。

算法分析

  • AC自动机的三个过程:

    1.插入模式串 构建Trie树
    2.设置失败节点
    3.对目标串进行匹配

1. 构建Trie树

正常的构建过程,每插入一个模式串对每个字符进行遍历,如果节点不存在就创建,继续向下层拓展。(结尾的节点需要特殊标志)
假设模式串有:she , he ,say, her, shr
我们要构建一棵字典树:


【AC自动机】AC自动机可以帮你自动AC吗_第1张图片

2.设置失败节点

设置失败节点是AC自动机的关键所在,在KMP算法中失败数组,指向的是失配之后继续匹配的位置,最终的效果就是对目标串扫描一遍,不需要回溯。而KMP中找的是最长公共前后缀。AC自动机找的是父节点的失败节点的子节点。
我们定义

struct {
      Node* father  //父节点
      Node* fail  //失败节点
      Node* children[26]  //孩子节点
}Node
注:该代码为了理解,与实际节点有所不同

我们通过bfs遍历节点,每个节点处理完毕后,孩子入队,直到队列为空。
一般情况:
假设当前节点为p,那么我们需要找到p->father ->fail ->child,查看child是否有与p节点字母相同的节点,若有 p->fail = p->father->fail->children[与p相同],若无我们要寻找p->father->fail->fail,直到找到与p对应的child节点或达到root节点。
第一层节点:
p->fail = root;
(实际的代码编写中,并不需要设置父节点的指针,可以认为p是父节点,对他的children节点进行遍历设置,操作将会是p->children[i]->fail = p->fail->children)
失败节点的正确性:失败节点的特点是与原节点的字母相同,那么我们找到父节点的失败节点,找到的节点与原节点的父节点相同。那么父节点的失败节点(p_father->fail)找的是父节点的父节点(p_father->father),这样以此类推,查找的路径都将是正确的。

  • 一个例子
    【AC自动机】AC自动机可以帮你自动AC吗_第2张图片

    首先我们将root入队。

  • 第一层(fail为红色虚线):
    root出队,遍历root所有的子节点h,s使之fail指向root,并且将h,s入队。
  • 第二层(fail为蓝色虚线):
    h出队,找到e,找到h节点的失败节点root,root节点下无e节点,因为节点为root,e的fail指向root,e入队。
    s出队,找到a,找到s节点的失败节点root,root节点下无a节点,因为节点为root,a的fail指向root,a入队。找到h,找到s节点的失败节点root,root下有h节点,第三层的h节点的fail指向root节点的子节点h。h入队。
  • 第三层(fail为绿色虚线):

对目标串进行匹配

从root节点开始

  • 目标串匹配的两种情况:
    1. 对应字符子节点存在
      a. 为结尾节点,计数后,对该节点进行一次fail节点遍历到root,并对每个fail节点计(这次遍历不影响当前匹配节点)
      b. 不为结尾节点,寻找子节点是否有下一个字符
    2. 对应字符子节点不存在
      a. 该节点不是root节点,寻找该节点的fail节点
      b. 该节点是root节点,寻找子节点是否有下一个字符
      不断重复以上过程,遍历直到完目标串。
  • 一个例子

    【AC自动机】AC自动机可以帮你自动AC吗_第3张图片
    这是上面我们已经构造好的AC自动机
    我们现在用目标串:yasherhs进行匹配,设匹配节点p,初始节点p=root,当i=0,1时匹配不成功。p依旧指向root。
    当i=2,3,4时,p节点匹配路径为she,p指向了第三层的e节点,计数一次(1),现在构建临时的一个temp节点,temp=p,去找temp->fail,发现temp->fail也为结尾,计数一次(2)。继续找temp->fail->fail,找到root结束。继续遍历目标串。
    当i=5时,继续寻找p节点的子节点,没找到r节点,p=p->fail,我们找到了第三层的e节点,找到子节点r,p=r,r为结尾节点,计数一次(3)
    当i=6时,未找到h子节点,p回到了r->fail节点root。
    当i=7时,找到h节点但并不是结尾节点。i=8回到root。结束。

  • 我们能发现最后成功匹配的三次,在匹配的时候对具体代码进行调整,也实现每个模式串只匹配一次,记录匹配成功的串等操作。
    经典的AC自动机是判断模式串出现的个数,因此在每次成功的时候会对节点进行已访问的标记。

代码实现

经典AC自动机

代码来自源AC自动机算法详解 (转载)

  • 基本数据结构
const int kind = 26; 
struct node
{
    node *fail;       //失败指针
    node *next[kind]; //Tire每个节点的个子节点(最多个字母)
    int count;        //是否为该单词的最后一个节点
    node()            //构造函数初始化
    {
        fail=NULL;
        count=0;
        memset(next,NULL,sizeof(next));
    }
}*q[500001];          //队列,方便用于bfs构造失败指针
char keyword[51];     //输入的单词
char str[1000001];    //模式串
int head,tail;        //队列的头尾指针
  • 构建trie树
void insert(char *str,node *root){ 
    node *p=root;
    int i=0,index;
    while(str[i])
    {
        index=str[i]-'a';
        if(p->next[index]==NULL) p->next[index]=new node();
        p=p->next[index];
        i++;
    }
    p->count++;     //在单词的最后一个节点count+1,代表一个单词
}
  • 设置失败节点
 void build_ac_automation(node *root){
    int i;
    root->fail=NULL;
    q[head++]=root;
    while(head!=tail)
    {
        node *temp=q[tail++];
        node *p=NULL;
        for(i=0; i<26; i++)
        {
            if(temp->next[i]!=NULL)
            {
                if(temp==root) temp->next[i]->fail=root;
                else
                {
                    p=temp->fail;
                    while(p!=NULL)
                    {
                        if(p->next[i]!=NULL)
                        {
                            temp->next[i]->fail=p->next[i];
                            break;
                        }
                        p=p->fail;
                    }
                    if(p==NULL) temp->next[i]->fail=root;
                }
                q[head++]=temp->next[i];
            }
        }
    }
}
  • 目标串匹配
int query(node *root){ 
    int i=0,cnt=0,index,len=strlen(str);
    node *p=root;
    while(str[i])
    {
        index=str[i]-'a';
        while(p->next[index]==NULL && p!=root) p=p->fail;
        p=p->next[index];
        p=(p==NULL)?root:p;
        node *temp=p;
        while(temp!=root && temp->count!=-1)
        {
            cnt+=temp->count;
            temp->count=-1;
            temp=temp->fail;
        }
        i++;
    }
    return cnt;
}

你可能感兴趣的:(【AC自动机】AC自动机可以帮你自动AC吗)