字符串学习笔记(3) – AC自动机

AC自动机(Aho-Corasick Automation) 是以 Trie 作为结构基础,结合 KMP 的思想 建立的自动机,用于解决多模式匹配等任务。

简单的说,我们要对 Trie 上的每个状态 $u$ 构造一个失配指针(fail),指向状态 $v$ ,满足 $v$ 是 $u$ 的最长后缀。然后我们就可以像 KMP 算法那样,在 Trie 上完成多模式匹配任务。

建立AC自动机主要有两个步骤:

  1. 将所有模式串插入Trie中
  2. 利用KMP算法思想,对Trie上所有节点构造fail指针

而构造fail指针的具体过程也类似于KMP算法。我们使用bfs来实现fail指针的构建,考虑状态 $u$ ,设其通过字符 $c$ 的边指向状态 $v$ ,即$next(u,c) = v$ ,因为是bfs,所以我们可以保证$fail(u)$的fail指针一定已经被构建完毕 。如果 $u$ 可以接受字符 $c$ ,也就是状态$v$ 存在,那么直接可以令 $fail(v) = fail(next(fail(u),c))$;如果 $u$ 无法接受字符 $c$ ,即 $v$ 不存在,那么我们令 $next(u,c) = next(fail(u),c)$.特别的, $fail(0) = 0$。

偷张oiwiki的示意图

建Trie树与构建fail指针部分的代码:

void insert(const string &s) {
    int p=0;
    for(auto c : s) {
        if(!t[p].nxt[c-'a']) t[p].nxt[c-'a']=++tot;
        p=t[p].nxt[c-'a'];
    }
    t[p].end+=1;
    return;
}
void bfs() {
    queue<int> q;
    for(int i=0;i<26;i++) if(t[0].nxt[i]) q.push(t[0].nxt[i]);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        for(int i=0;i<26;i++) {
            int &v=t[u].nxt[i];
            if(v) t[v].fail = t[t[u].fail].nxt[i],q.push(v);
            else v = t[t[u].fail].nxt[i];
        }
    }
}

这样我们就完成了AC自动机的构建,时间复杂度为 $O(n| \sum |)$,其中 $| \sum |$ 为字符集大小。

我们发现,构建完AC自动机的Trie树实际上变成了一张图,也就是 字典图,并且原Trie树的边与新建的fail边分别构成了一棵树。在查询的时候,对于一个状态 $s$ ,我们从 $s$ 开始不断沿fail边向上跳,根据定义,fail边指向的每一个状态都可以与 $s$ 相匹配,所以只需要在跳的过程中不断累积答案,并将经过的状态打上标记。

int query(const string &s) {
    int p=0,ans=0;
    for(auto c : s) {
        p=t[p].nxt[c-'a'];
        for(int j=p;j && t[j].end != -1;j=t[j].fail) {
            ans+=t[j].end;
            t[j].end=-1;
        }
    }
    return ans;
}

下面我们再来看一个问题:


给你一个文本串 $S$ 和 $n$ 个模式串 $T_{1 \sim n}$,请你分别求出每个模式串 $T_i$ 在 $S$ 中出现的次数。

因为要统计次数,所以我们不能再在跳 fail 边时打标记了。如果每次都强行跳 fail 边,复杂度会退化到 $O(nm)$。那怎么办呢?我们要思考AC自动机的本质。我们每次统计答案,都是在将fail树的一条整链上的节点答案加一。我们可以用一些处理树上数据结构的技巧,只在起跳节点加上一,然后通过拓扑排序,将根节点的答案传递到子树中,这样就避免了反复跳 fail 边导致的复杂度退化。

void solve(string s) {
    int p = 0;
    for(auto c : s) {
        p = t[p].nxt[c-'a'];
        t[p].num++;
    }
}
void topsort() {
    queue<int> q;
    for(int i=1;i<=tot;i++) if(!d[i]) q.push(i);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        for(auto id : t[u].v) ans[id] = t[u].num;
        int v = t[u].fail;
        t[v].num+=t[u].num;
        if(!(--d[v])) q.push(v);
    }
}

此外,还可以通过子树求和的方法来优化复杂度,具体可查阅 oiwiki,此处暂略不提。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇