[模板] AC自动机

AC 自动机

AC 自动机 (Aho-Corasick Automation) 是一个多模式字符串匹配算法.

定义 \(fail\) 函数为该状态 (节点) 代表的字符串的最长后缀所在的状态. 特别的, 如果不存在这样的状态, \(fail\) 函数设为根.

为了提高构建和匹配的效率, 在建立 trie 树之后, 定义

\[child(p, c) = \begin{cases} v & (p 存在 c 的转移) \\ child(fail(p), c) & (不断跳 fail 函数到第一个有 c 的转移的状态) \\ root & (不存在这样的状态) \end{cases} \]

也可以把这种定义了新的 \(child\) 转移的 AC 自动机称为 trie 图.

这样的话, 设 \(N\) 为 trie 树的节点数, \(\Sigma\) 为字符集大小, 模式串的长度和为 \(T\), 文本串的长度为 \(S\),
构建的时间复杂度为 \(O(N \cdot \Sigma)\), 空间复杂度为 \(O(N \cdot \Sigma)\);
匹配的时间复杂度为 \(O(S)\).

另一种实现是:

不补全不存在的转移, 并且利用 unsigned int 压位维护 \(\Sigma\) 个转移是否存在, 求 \(fail\) 函数 / 匹配的过程直接跳 \(fail\) 函数.

可以利用势能分析证明, 这样的时间复杂度为 \(O(T)\).

代码

const int ndsz=1e6+50;
struct tac{
    int ch[ndsz][30],fi[ndsz],end[ndsz],pn=0,rt=0;
    int tr(int v){return v-'a'+1;}
    int newnd(){return ++pn;}
    void clear(){
        memset(ch,0,sizeof(ch));
        memset(fi,0,sizeof(fi));
        memset(end,0,sizeof(end));
        pn=0,rt=0;
    }
    void insert(char *s){
        int p=rt,v;
        for(int i=0;s[i];++i){
            v=tr(s[i]);
            if(ch[p][v]==0)ch[p][v]=newnd();
            p=ch[p][v];
        }
        ++end[p];
    }
    void build(){
        static int qu[ndsz],qh=1,qt=0;
        rep(i,1,26)if(ch[rt][i])fi[ch[rt][i]]=0,qu[++qt]=ch[rt][i];
        while(qh<=qt){
            int u=qu[qh++];
            rep(i,1,26){
                if(ch[u][i]==0)ch[u][i]=ch[fi[u]][i];
                else fi[ch[u][i]]=ch[fi[u]][i],qu[++qt]=ch[u][i];
            }
        }
    }
    int match(char *s){
        int now=rt,ans=0;
        for(int i=0;s[i];++i){
            now=ch[now][tr(s[i])];
            for(int p=now;p&&~end[p];p=fi[p])ans+=end[p],end[p]=-1; //每个模式串只匹配一次
        }
        return ans;
    }
    void pr(){
        printf("pn=%d,rt=%d\n",pn,rt);
        rep(i,0,pn){
            printf("fi=%d,end=%d,",fi[i],end[i]);
            rep(j,1,26)printf("%d ",ch[i][j]);
            printf("\n");
        }
    }
}ac;

附加的应用

DP

由于 AC 自动机属于 DAG , 可以在自动机上进行动态规划.

last指针

有时题目所求仅与关键点 (模式串终点) 有关. 可以定义 \(last(p)\) 表示 \(p\) 跳 fail 指针到达的第一个关键点.

\(last(p)\) 容易通过动态规划求出.

fail 树

将所有fail指针反向, 可以得到一棵以原 trie 树的根为根的树, 可以称之为 fail 树.

fail树有如下的性质:

  1. 每个节点代表某个模式串的一个前缀;
  2. 节点的父亲代表它的满足 1. 的最长后缀.

我们还可以得到这样的一些结论:

  1. 一个节点的子树大小即为它在所有模式串中出现的次数.
  2. ...
posted @ 2018-10-29 20:10  Ubospica  阅读(190)  评论(0编辑  收藏  举报