AC自动机

来源:互联网 发布:mac电子书导入kindle 编辑:程序博客网 时间:2024/05/29 07:56

  • AC自动机
    • 简介
    • 前提知识
    • 问题
    • 分析
    • AC自动机
      • 构建TrieTrie
      • 构建fail指针
      • 匹配
    • 练习

AC自动机

简介

AhoCorasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。下面我们会举一个具体问题来讲解AC自动机。

前提知识

  • KMP算法
    对于问题:
    找出长度l的字符串在长度为m的字符串中出现次数?
    KMP给出了O(l+m)的做法
  • Trie
    n个单词,按照单词的前缀来把n个单词构成一棵树,拥有相同前缀的单词在同一个分支上。
    举个例子:
    对于单词say,her,he,shr,she
    我们构建Trie
    这里写图片描述

问题

给出n个单词p1,p2,p3,......,pn,每个单词的长度为l1,l2,l3,......,ln。再给出一段包含m个字符的文章S,让你找出有多少个单词在文章里出现过。

分析

不妨假设每一个单词的长度是O(l)级别的长度

  1. 原始想法
    我们将每一次单词piS进行匹配,然后判断这个单词在不在该文章中。此时我们可以看出来这样做的复杂度是O(ni=1piS)=O(nlS)。显然这个复杂度太高了。
  2. 优化
    每一次我们都需要将S与单词进行匹配,这个时候我们是可以使用KMP算法来完成。那么我们就可以把整个复杂度降低到O(n(l+S)),显然这个复杂度要优化很多。
  3. 再优化
    我们是不是真的需要独立地去匹配每一个piS?如果我们在所有模式串中找到了一定的联系,当S与其中一个匹配失败时,能够直接(或者说快速地)指向另外一个模式串,那我们将大大降低复杂度。再次利用KMP思想,我们把所有的模式串构建成一棵Trie树,然后KMP中失配时的next指针进行改成映射到树中某一个节点的fail指针。这样再进行匹配,那么总的复杂度就会变成O(nl+S)

AC自动机

AC自动机的算法过程:

  1. n个模式串构建Trie 复杂度O(nl)
  2. 构建fail指针 O(nl)
  3. 进行匹配 O(nl+S)

构建Trie

我们如上图所示建立一棵Trie,代码如下:

void insert(char* str){    int len = strlen(str);    int u = rt;    FOR(i,0,len){        if(ch[u][str[i]-'a'] == -1){            newnode();            ch[u][str[i]-'a'] = sz;        }        u = ch[u][str[i]-'a'];    }    ++ val[u];}

val数组用来记录该节点是不是结尾节点,即该分支是不是一个单词。很容易看出来,建立过程复杂度为O(nl)

构建fail指针

我们用bfs来构建fail指针,首先了解fail指针干了一件什么事情?fail指针保证了能以O(1)的复杂度找到能与当前分支所构成的字符串匹配的最长公共后缀。
换句话说,如果当前分支为roota1a2a3......al,其中fail[al]=bp,那么fail指针指向的分支为rootb1b2b3......bpfail指针保证了ali==bpi对于任意i<p恒成立。
接下来我们用bfs来构造fail指针。
首先第一层的所有节点的fail指针都指向root,分别进入队列,然后s进入队列,查看子节点a,找到fail[s]的位置,看看ch[fail[s]][a]是否存在,如果存在,直接指向fail[s]下面的a子节点,不然指向fail[fail[s]],继续查看,直到fail[s]=root仍然匹配失败,就指向root,否则指向ch[fail[s]][a],即fail[ch[s][a]]=ch[fail[s]][a]。根据这个规则,我们发现a指向root,然后a进队列,查看h子节点,发现rooth则,指向root下的h节点。依次构建,形成了下图中的fail指针图。
这里写图片描述
以下代码做了做了优化:

void build(){    queue <int> q;    FOR(i,0,26){        if(ch[rt][i] == -1){            ch[rt][i] = rt;        }        else{            fail[ch[rt][i]] = rt;            q.push(ch[rt][i]);        }    }    while(!q.empty()){        int u = q.front();  q.pop();        FOR(i,0,26){            if(ch[u][i] == -1){                ch[u][i] = ch[fail[u]][i];            }            else{                fail[ch[u][i]] = ch[fail[u]][i];                q.push(ch[u][i]);            }        }    }}

很容易看出复杂度为O(nl)

匹配

设我们要记录的答案为ans
以字符串S=yasherhes为例,我们扫描S串。设扫描到了S[i]点,当前Trie所在的节点为u。分为两种情况:

  1. ch[u][S[i]]存在,那么继续向下扫描,u变成ch[u][i]S[i]点变成S[i+1]
  2. ch[u][S[i]]不存在,那么u=fail[u],继续匹配ch[u][S[i]],直到:
    2.1 有一个节点匹配,那么u变成ch[u][i],S[i]点变成S[i+1]
    2.2 如果没有节点匹配,那么u变成rootS[i]点变成S[i+1]

按照以上规则匹配字符串SS[0]=y字符失配,此时u=root=0。接下来匹配S[1]=a,继续失配。匹配S[2]=s,此时u=1。继续匹配S[3]=h,此时u=7。继续匹配S[4]=e,此时u=9,发现这个节点是一个单词结尾,即en[9]!=0,那么我们可以修改答案ans=ans+1,修改en[9]=0。继续匹配S[5]=r,失配,那么u=fail[9]=5,继续失配,u=fail[5]=0,失配,u=root。匹配S[6]=hu=4。继续匹配S[7]=eu=5ans=ans+1。匹配S[8],失配。结束,那么答案就是2
附上代码:

int query(char* str){    int len = strlen(str);    int now = rt;    int res = 0;    FOR(i,0,len){        now = ch[now][str[i]-'a'];        int temp = now;        while(temp != rt){            res += val[temp];            val[temp] = 0;            temp = fail[temp];        }    }    return res;}

这里的复杂度就是O(nl+S)

练习

HDU 2222 Keywords Search

原创粉丝点击