Aho-Corasick automation,該算法在1975年產生於貝爾實驗室,是著名的多模匹配算法之一。一個常見的例子就是給出 \(n\) 個單詞,再給出一段包含 \(m\) 個字符的文章,讓你找出有多少個單詞在文章裏出現過。要搞懂AC自動機,先得有模式樹(字典樹)\(Trie\) 和 \(KMP\) 模式匹配算法的基礎知識。html
這裏的Trie可不是什麼權值線段樹
就是常說的字典樹。
這個東西彷佛很簡單?不會的出門左轉問度娘
一個性質是兩個串的\(LCP\)就是這兩個串對應結束位置的\(LCA\)ios
(此圖蒯自網絡)(右上方的幾個字符串是模式串)
這就是一個\(AC\)自動機。
想要知道自動機是什麼的大佬請問度娘
它大致上就是一個 \(Tire\) 樹(Trie樹中字符應該在邊上,但這裏字符在點上不引響解釋),不過多了一些奇怪的東西,也就是圖中的虛箭頭。
這叫作fail指針。\(Trie\) 樹中一個點的 \(fail\) 指針指向這個點表明字符串的最長後綴對應的節點。 \(fail\) 指針體現了 \(KMP\) 的思想
能夠結合上面的圖理解。算法
咱們發現若是咱們沿着一個點的 \(fail\) 邊一直跳,就能夠遍歷這個點表明字符串的全部後綴。用AC自動機解決的經典問題:多模式串匹配,就使用了AC自動機的這個性質。網絡
咱們發現 \(fail\) 邊構成了一棵樹(以後叫作fail 樹),其中一個點的子樹中全部的點表明的串都包含這個點表明的串(或者說這個點表明的串是這個點的 fail 樹上子樹中的點表明的串的子串)。 51nod 麥克打電話這個題就運用了AC自動機的這個性質。優化
接下來說講如何構造\(AC\)自動機。
\(AC\)自動機就是\(Trie\)樹上多了一些\(fail\)邊,固然要先建出\(Trie\)樹。spa
void insert(char *s){ int l=strlen(s+1); int now=0; for(int i=1;i<=l;i++){ if(ac[now].nxt[s[i]-'a']==0)ac[now].nxt[s[i]-'a']=++tot; now=ac[now].nxt[s[i]-'a']; } ac[now].cnt++; }
(PS:代碼中ac[u].nxt[i]
表明 \(Trie\) 樹中 \(u\) 節點沿 \(i\) 字符這條邊走到達的節點,ac[u].cnt
表明 \(Trie\) 樹中以 \(u\) 爲結束爲止的字符串數量)
而後就是如何建 \(fail\) 邊了。
考慮按長度遞增的順序(例如 \(bfs\) 序)依次求每一個節點的\(fail\)。假設節點 \(x\) 的父邊上的字符是 \(c\),那麼咱們就從\(fail[fa[x]]\) 開始沿着 \(fail\) 鏈往上跳,直到跳到一個節點 \(y\) 使得 \(y\) 有字符爲 \(c\) 的出邊,那麼這條出邊走到的兒子就是 \(fail[x]\)。指針
void get_fail(){ queue<int> q; for(int i=0;i<=25;i++) if(ac[0].nxt[i])q.push(ac[0].nxt[i]); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=0;i<=25;i++) if(ac[u].nxt[i]){ q.push(ac[u].nxt[i]); int now=ac[u].fail; while(now&&ac[now].nxt[i]==0)now=ac[now].fail; if(now==0&&ac[now].nxt[i]==0)ac[ac[u].nxt[i]].fail=0; else ac[ac[u].nxt[i]].fail=ac[now].nxt[i]; } } }
(PS:代碼中ac[u].fail
表明 \(u\) 點的 \(fail\) 邊指向節點。code
考慮對於屬於同個串的節點建 \(fail\) 的總複雜度,\(KMP\) 用的分析依然適用,所以時間複雜度爲總串長。htm
以這道模板題爲例
把文本串在 \(Trie\) 上進行匹配,新加一個字符時,若當前節點沒有這個字符的出邊,就一直沿着 \(fail\) 往上跳,直到跳到一個有該字符的出邊爲止,而後走到出邊指向的兒子。而後把節點記錄的結束位置個數統計一下就行了。blog
查詢時間複雜度爲文本串長度。複雜度分析同 \(KMP\) 複雜度分析
#include<iostream> #include<cstring> #include<cstdio> #include<cmath> #include<algorithm> #include<queue> using namespace std; const int N=1001000; int n,ans,tot; char s[N]; struct AC{ int nxt[26],cnt,fail; }ac[N]; void insert(char *s){ int l=strlen(s+1); int now=0; for(int i=1;i<=l;i++){ if(ac[now].nxt[s[i]-'a']==0)ac[now].nxt[s[i]-'a']=++tot; now=ac[now].nxt[s[i]-'a']; } ac[now].cnt++; } void get_fail(){ queue<int> q; for(int i=0;i<=25;i++) if(ac[0].nxt[i])q.push(ac[0].nxt[i]); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=0;i<=25;i++) if(ac[u].nxt[i]){ q.push(ac[u].nxt[i]); int now=ac[u].fail; while(now&&ac[now].nxt[i]==0)now=ac[now].fail; if(now==0&&ac[now].nxt[i]==0)ac[ac[u].nxt[i]].fail=0; else ac[ac[u].nxt[i]].fail=ac[now].nxt[i]; } } } void work(char *s){ int l=strlen(s+1); int now=0; for(int i=1;i<=l;i++){ while(now&&ac[now].nxt[s[i]-'a']==0)now=ac[now].fail; now=ac[now].nxt[s[i]-'a']; for(int y=now;ac[y].cnt!=-1;y=ac[y].fail){ ans+=ac[y].cnt; ac[y].cnt=-1; } } } int read(){ int sum=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();} return sum*f; } int main(){ n=read(); for(int i=1;i<=n;i++){ scanf("%s",s+1); insert(s); } get_fail(); scanf("%s",s+1); work(s); printf("%d",ans); return 0; }
假如節點 \(x\) 沒有字符 \(c\) 的出邊,那麼不妨找到 \(x\) 在 \(fail\) 樹上最近的一個有字符 \(c\) 出邊的祖先,從 \(x\) 連一條邊到這個點的字符 \(c\) 的兒子。這些邊以及本來 \(Trie\) 樹的結構構成的轉移圖稱爲 \(Trie\) 圖。下面記 \(trans[x][c]\) 表示從節點 \(x\) 連出的字符 \(c\) 的出邊指向的節點。
Trie 圖則是至關於把 NFA 轉化爲了 DFA(這涉及到了自動機的概念,不瞭解也不影響對本篇文章的閱讀,有能力的能夠了解一下,能夠加深理解還能夠用來裝逼)
建圖的時候,\(fail[x]\) 就是 \(trans[fail[fa[x]]][c]\)。若是 \(x\) 自己有字符 \(c\) 的兒子,那麼 \(trans[x][c]\) 就是這個兒子,不然\(trans[x][c]\) = \(trans[fail[x]][c]\)。
時間複雜度 \(O(|T||∑|)\),其中 \(|T|\) 表示 \(Trie\) 的大小,\(|∑|\)表示字符集大小。
下面是Trie圖優化的代碼,仍是剛纔那個模板題
#include<iostream> #include<cstring> #include<cstdio> #include<cmath> #include<algorithm> #include<queue> using namespace std; const int N=1001000; int n,ans,tot; char s[N]; struct AC{ int nxt[26],cnt,fail; }ac[N]; void insert(char *s){ int l=strlen(s+1); int now=0; for(int i=1;i<=l;i++){ if(ac[now].nxt[s[i]-'a']==0)ac[now].nxt[s[i]-'a']=++tot; now=ac[now].nxt[s[i]-'a']; } ac[now].cnt++; } void get_fail(){ queue<int> q; for(int i=0;i<=25;i++) if(ac[0].nxt[i])q.push(ac[0].nxt[i]); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=0;i<=25;i++) if(ac[u].nxt[i])q.push(ac[u].nxt[i]),ac[ac[u].nxt[i]].fail=ac[ac[u].fail].nxt[i]; else ac[u].nxt[i]=ac[ac[u].fail].nxt[i]; } } void work(char *s){ int l=strlen(s+1); int now=0; for(int i=1;i<=l;i++){ now=ac[now].nxt[s[i]-'a']; for(int y=now;ac[y].cnt!=-1;y=ac[y].fail){ ans+=ac[y].cnt; ac[y].cnt=-1; } } } int read(){ int sum=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();} return sum*f; } int main(){ n=read(); for(int i=1;i<=n;i++){ scanf("%s",s+1); insert(s); } get_fail(); scanf("%s",s+1); work(s); printf("%d",ans); return 0; }
本文只是講解算法,真正掌握它還須要多刷題。
完結撒花