AC快乐机

AC快乐机

众所周知,KMP是算法竞赛中常用的字符串匹配算法,该算法通过对模式串构建next数组的方式,十分有效的提高了匹配的效率。

单一模式串的匹配可以构造next,那如果模式串有多个,也同样能通过构造next的方式匹配吗?

小企鹅

Fail指针

给你多个模式串,也就是给你一棵Trie,在Trie上进行匹配。假设我们有能力构建出一棵Trie的next,考虑KMP中的next的定义(指向最长的后缀),那么一棵Trie的next是长这样的(以模式串he,she,him,hers,shit为例):

这是个Trie

这棵Trie的next是这样的(没画的都指向根):

它的fail

现在要拿主串ashers上去匹配,匹配的方式和KMP一样:如果可以接着向下走则向下走,否则爬next

那么匹配的路径为root-root-s-h-e-e-r-s,最终匹配成功

匹配的复杂度显然是 O ( n + m i n ( n , m ) ) 的(其中 n 为主串长度, m 为最长的模式串长度)

如果在Trie树上能构造出next的话,匹配复杂度就会变成线性,这就是AC自动机基础的原理啦,AC自动机中next的名字叫做Fail指针(也称后缀节点),接下来的所有next都会用Fail代替

Fail指针是从一个Trie节点(对应字符串 α )到另一个Trie节点(对应的字符串 ω α 的最长后缀)的有向边

匹配时,如果我们到达了不能继续匹配的节点,我们沿着Fail指针爬以维持尽可能更大的前缀

对于Trie树上每一个节点,我们都可以构造一个Fail指针

Fail指针的构造?

Fail指针的些许小性质:

1.根的所有儿子的Fail都是根

2.节点x的Fail指向y(非根),那么x的t儿子的Fail指向y的t儿子的Fail

以上图为例,sheh的Fail指向的是he中的h,那么she儿子she的Fail就会指he儿子he

3.一个节点一直沿着Fail爬会爬到根,期间深度是不断减小的

这样我们有了一个构建Fail的大体思路,对于每一个点,对他所有的儿子构建Fail,然后bfs下去,对t儿子构建Fail的过程可以通过不断爬Fail找到第一个有t儿子的点这一途径得到

但也会有一些不美妙的情况使这个过程变得很不美观

一个栗子

假设上图苟利国家生死以有一个儿子,用刚才所说的方法构造的Fail指针就需要沿着蓝色箭头爬好远好远好远好远好远好远才能找到,这个过程会让过程及复杂度极其不优美,AC自动机用Trie图简化了这个过程

Trie图

(为了方便,将根所有不存在的儿子都设为根)

对于Trie树上的每一个节点 x ,它的 α 边指向的是它下一个字符 t ,表示如果当前状态是 x ,待匹配字符是 α ,那么匹配到的点是 t

设字符集为 S ,点 x 可匹配的(向外延伸出去的)字符集 S S ,即 α S ,都可以 x (不通过爬Fail的方式)由向外延伸(到 p )

此过程描述为:当 x 的下一个字符是 α ( α S )时,走到的节点是 p

但是如果当前状态是 x ,下一个字符 β S ,也就代表着在此失配,那么根据刚才所描述的方法,AC自动机需要爬Fail找到第一个有 β 儿子的点 y 继续匹配(到 q )。

此过程可以描述为:当 x 的下一个字符是 β ( β S )时,走到的节点是 q

这两个过程描述起来十分相似,那么我们为什么不将 q 直接看作 x β 儿子呢?

对于节点 x ,如果它并没有 β 儿子,那么沿着找到第一个有 β 儿子的节点 y ,并把 y β 儿子看作是自己的 β 儿子

我们把这个操作称为NTR,NTR过后的图叫Trie图

上图理论上的Trie图是长这样的

Trie图

但实际上是这样的

真实Trie图

如果 x NTR了 y α y NTR了 z α ,可以直接看作 x NTR了 z α ,也就是说如果我们按照Fail的顺序构建,NTR操作只要考虑一个Fail就可以了

在Trie图上的匹配只要无脑向后走就可以了,也就是说,构建完Trie图,Fail就没什么用了

Trie图本质上是个有向图

Fail指针 Trie图的构造!

struct Node{
    Node *ch[26],*fail;
    bool b;
    Node():fail(NULL){
        b=false;
        for(int i=0;i<26;i++)
            ch[i]=NULL;
    }
}*root=new Node;
queue<Node*>q;
inline void Insert(char *s){///构建Trie
    Node *x=root;
    int len=strlen(s+1);
    for(int i=1;i<=len;i++){
        if(!x->ch[s[i]-'0']) x->ch[s[i]-'0']=new Node;
        x=x->ch[s[i]-'0'];
    }
    x->b=true;
}
inline void GetFail(){///AC自动机可以以bfs的方式构建
    root->fail=root;///根的Fail是他自己
    for(int i=0;i<26;i++){
        if(root->ch[i]) q.push(root->ch[i]),root->ch[i]->fail=root;
        ///将根儿子的Fail指向根,并放入队列里
        else root->ch[i]=root;
        ///如果root没有i这个儿子,就把他的儿子赋为自己,这样以上的性质2对根也满足了
    }
    while(!q.empty()){
        Node *x=q.front();q.pop();
        for(int i=0;i<26;i++){
            if(x->ch[i]) x->ch[i]->fail=x->fail->ch[i],q.push(x->ch[i]);
            ///根据性质2搞出各个儿子的Fail
            else x->ch[i]=x->fail->ch[i];///NTR操作(
        }
        Node *tmp=x->fail;
        while(tmp!=root && !tmp->b) tmp=tmp->fail;
        if(tmp->b) x->b=true;
        ///如果它的某一后缀是一个模式串,那么它也肯定包含模式串(一些特定的题统计时会用到)

    }
}

Fail树

把所有Fail指针当作一条无向边,可以发现构成的是一个树形结构

小例题:

给定一个n个单词的文章,求每个单词在文章中的出现次数 ( n 200 ,单词总长 10 6 )

小企鹅

大体思路:先将所有的串建一个AC自动机,构建的Fail时候到一个点就将所有它的所有Fail都加上1

显然会T

可以发现,一个点一定是它Fail树中子树点的后缀(如果串s能匹配到这里,s就会是子树所有点的后缀)

那么一个串的出现次数事实上就是它Fail树子树中单词数量的和

然后没了

猜你喜欢

转载自blog.csdn.net/waduan2/article/details/80688515
ac