【笔记】AC自动机

168 阅读4分钟

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战.


多模式匹配问题:给定若干个模式串,每次询问给定一个文本串,问各模式串在文本串中各出现了多少次。

AC自动机最典型的应用就是求解多模式匹配问题,此外通过与DP组合可以求解其它问题。


前置知识:

  1. Trie树:又叫字典树,每条边上都有一个字母。
  2. fail指针:fail[i]表示与以i节点为结尾的串的后缀有最大公共长度的前缀的结尾编号,fail的思想来自于kmp算法。

构建: 首先将各模式串组成一棵Trie树,标记所有终结点。然后在Trie树上按bfs序访问每个节点,操作如下:

  1. 建立bfs用的队列,将根节点所有存在子节点的fail设置成根节点,然后把他们加入队列中。
  2. 从队列里取出当前节点,遍历26(字符集大小,默认26)个子节点,如果存在,则将子节点的fail设置成当前节点fail的对应子节点,并将子节点加入队列中。
  3. 如果不存在,则将这条边连向当前节点fail的对应子节点。
  4. 队列非空时,返回2.

构建完成之后,由于第三步的存在,Trie树中的每个节点都会引出26条有向边,而且可能指向高层节点,现在的Trie结构由就Trie树变成了Trie图

构建过程中除了建立Trie图之外,还求出了每个节点的fail,把所有的fail指针拿出来就是一棵fail树

查询: Trie图和fail树就是AC自动机的核心结构。

fail树上根到每个节点所表示的字符串,都是根到它子节点所表示的字符串的后缀。 查询时类比Trie树查询,直接在Trie图上走就可以,可以时刻保证位置的正确性。

在Trie图上走时,每到一个节点,需要遍历fail树上从它到根的路径,每有一个带终结标记的节点,就表示匹配到了这个字符串一次。

优化: 针对多模式匹配查询的优化有两个:

  1. 建立fail树时,找到每个节点的祖先中最近的带终结标记的节点,然后直接连一条边,称为后缀链接,或者last指针,找到每个节点的last边后就建立了last树。匹配时,直接在last树上而非fail树上统计答案,可以有效降低时间复杂度。
  2. 在last树上统计答案的过程相当于每次给定一个节点,将根到它路径上所有的点权加一,最后只输出一个答案,可以使用树上差分来将复杂度优化成线性。

代码:

//fail[i]为与以i节点为结尾的串的后缀有最大公共长度的前缀的结尾编号
//注意字符集
struct Aho_Corasick_Automaton
{
    int ch[M][26]={}, end[M]={}, sz=0; //Trie树: end表示以i为结尾的串编号
    int fail[M]={}, last[M]={}; //AC自动机: 失配数组, 后缀连接
    int ans[M]={}; //答案存储
    
    int init(int id = 0)
    {
        memset(ch[id], 0, sizeof(ch[0]));
        fail[id] = last[id] = end[id] = 0;
        return sz = id;
    }

    // 向Trie树中尝试插入一个模式串, 返回插入后的编号
    int insert(int id, const char *s)
    {
        int u = 0;
        for(int i = 0; s[i]; i++)
        {
            int &v = ch[u][s[i] - 'a'];
            if(!v) v=init(sz+1);
            u = v;
        }
        if(!end[u]) end[u] = id; 
        return end[u];
    }

    // 构建AC自动机
    void build()
    {
        queue<int> q;
        for(int v:ch[0]) if(v)
            fail[v] = 0, q.push(v);
        while(!q.empty())
        {
            int u = q.front(); q.pop();
            for(int i = 0; i < 26; i++)
            {
                int &v = ch[u][i];
                if(v) 
                {
                    q.push(v);
                    fail[v] = ch[fail[u]][i];
                    last[v] = end[fail[v]] ? fail[v] : last[fail[v]]; //后缀链接
                }
                else v = ch[fail[u]][i]; //建立trie图
            }
        }
    }
    
    // 查询一个文本串中各模式串出现了几次, 保存在ans中
    void query(const char *s)
    {
        memset(ans, 0, sizeof(ans));
        int now = 0;
        for(int i = 0; s[i]; i++)
        {
            now = ch[now][s[i] - 'a'];
            int id = end[now] ? now : last[now];
            while(id) ++ans[end[id]], id = last[id];
        }
    }
}AC;

例题 本质上都是模板题,问法各有不同,可以多熟悉一下。

  1. luogu P3808 【模板】 AC自动机(简单版)
  2. luogu P5357 【模板】 AC自动机(二次加强版)
  3. luogu P3796 【模板】 AC自动机(加强版)
  4. HDU 2222
  5. HDU 2896
  6. HDU 3065

本文也发表于我的 csdn 博客中。