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;
}
}