[學習筆記]AC自動機



Aho-Corasick automaton

本文基本上是oiwiki的復制粘貼:https://oi-wiki.org/string/ac-automaton/

可能加上了自己感性理解

概述

AC 自動機是 以 TRIE 的結構為基礎 ,結合 KMP 的思想 建立的

建立AC自動機有兩個步驟:

  • TRIE:將所有的模式串構成一顆字典樹
  • KMP:對TRIE上所有的狀態構造失配指針

可以說,AC自動機是由字典樹和失配指針構成的

回顧KMP

  • KMP匹配算法可以在線性時間內判定字符串\(A[1-N]\)是字符串\(B[1-M]\)的子串,並求出字符串A在字符串B中各次出現的位置.有兩個數組:\(next,f​\)
  • \(next[i]​\)表示的是A中以i為結尾的非前綴子串A的前綴能夠匹配的最長長度
  • \(f[i]​\)表示的是B中以i為結尾的子串A的前綴能夠匹配的最長長度

一直說AC自動機就是樹上的KMP,那這兩者有什么關系呢?

字典樹insert()

字典樹上的每個節點代表有限自動機一個狀態,表示的是某個模式串的前綴。

編譯原理學得超級差..(

模式串的結尾狀態被稱為可接受狀態

字典樹的插入:

void insert(char *s){
   int u=0;
   for(int i=1;s[i];++i){
    if(!tr[u][s[i]-'a']) tr[u][s[i]-'a']=++tot;
    u=tr[u][s[i]-'a'];
   }
   e[u]++;
}

失配指針fail[]

AC自動機需要利用一個fail指針來輔助多模式串的匹配

狀態\(u\)的fail指針指向另一個狀態\(v\)\(v\)\(u​\)的最長后綴。這里的fail指針跟KMP的next指針一樣是在失配的時候使用,fail指針指向的是所有模式串的前綴中匹配當前狀態的最長后綴。AC 自動機在做匹配時,同一位上可匹配多個模式串。因為有多個模式串,所以會指向多個模式串中的一個.KMP算法只有一個模式串,不會指向另一個模式串.

如果模式串只有一個,是不是相當於KMP算法?

考慮字典樹中當前結點\(u\)\(u\)的父節點是\(p\)\(p\)通過字符c的邊指向\(u\),標記為\(tr[p,c]=u\)

假設深度小於\(u​\)的所有結點的fail指針構造完畢

  • \(tr[fail[p],c]\)存在:則讓\(u\)的fail指針指向\(tr[fail[p],c]\)。即在\(p\)\(fail[p]\)后面加一個字符c,對應\(u\)\(fail[u]\)
  • \(tr[fail[p],c]\)不存在: 則繼續找到\(tr[fail[fail[p],c]]]​\)。重復1的判斷過程
  • 如果真的沒有,就讓fail指針指向根結點

構建函數build()

build()的目標有兩個:一個是構建fail指針,一個是構建自動機

  • \(tr[u,c]\)表示從當前狀態\(u\)后面加一個字符c能到達的狀態,即一個狀態轉移函數
  • q隊列,用於BFS遍歷字符串
  • \(fail[u]​\)結點\(u​\)的fail指針
    queue<int> q;

    void build() {
        for (int i = 0; i < 26; ++i){
            if (tr[0][i]) {
                q.push(tr[0][i]);
            }
        }
        while (!q.empty()) {/*每次從隊列中取出的u,表示fail[u]已經求出*/
            int u = q.front();
            q.pop();
            for (int i = 0; i < 26; ++i) {
                if (tr[u][i]) {/*這里表示的就是1過程,一行代碼搞定*/
                    fail[tr[u][i]] = tr[fail[u]][i];
                    q.push(tr[u][i]);
                } else {
                    tr[u][i] = tr[fail[u]][i];/*這行代碼會改變樹的結構,因為改變了tr數組,好處是節省時間,不需要跳那么多次fail,可以理解為路徑壓縮,直接到下一個能匹配的位置*/
                }
            }
        }
    }

fail指針的作用可以理解為舍棄前綴,指向下一個匹配的位置.

現在可能有個問題是對於kmp而言,那個板子求next是需要一個while循環,但是ac自動機的這個板子求fail只需要一行代碼.

我查過存在遞歸查找的板子,本文板子我的感性理解應該是因為空間換時間,將trie樹變成圖 ,采用了遞推的方法.

多模式匹配query()

既然建好了圖,也就是ac自動機,直接把字符串t輸入到自動機去.

利用fail指針找出匹配的模式串.

在匹配字符串的過程中,我們會舍棄部分前綴達到最低限度的匹配.

int query(char *t) {
  int u = 0, res = 0;
  for (int i = 1; t[i]; i++) {
    u = tr[u][t[i] - 'a'];  // 轉移
    for (int j = u; j && e[j] != -1; j = fail[j]) {
      res += e[j], e[j] = -1;
    }
  }
  return res;
}

模板

namespace AC {
    int tr[N][26], tot;
    int e[N], fail[N];

    void insert(char *s) {
        int u = 0;
        for (int i = 1; s[i]; ++i) {
            if (!tr[u][s[i] - 'a']) tr[u][s[i] - 'a'] = ++tot;
            u = tr[u][s[i] - 'a'];
        }
        e[u]++;
    }

    queue<int> q;

    void build() {
        for (int i = 0; i < 26; ++i) {
            if (tr[0][i]) {
                q.push(tr[0][i]);
            }
        }
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            for (int i = 0; i < 26; ++i) {
                if (tr[u][i]) {
                    fail[tr[u][i]] = tr[fail[u]][i];
                    q.push(tr[u][i]);
                } else {
                    tr[u][i] = tr[fail[u]][i];
                }
            }
        }
    }

    int query(char *t) {
        int u = 0, res = 0;
        for (int i = 1; t[i]; i++) {
            u = tr[u][t[i] - 'a'];  
            for (int j = u; j && e[j] != -1; j = fail[j]) {
                res += e[j], e[j] = -1;
            }
        }
        return res;
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM