一直想寫AC自動機了
但是考慮到學習AC自動機之前
還需要一點其他的知識的基礎
於是我先補充好了Trie樹和KMP的blog
如果以上兩個知識點沒有學好的話
請先學習這兩個知識點再來學習AC自動機
Trie(字典樹)
KMP算法
如果能夠解決上面的兩個 算法/結構 那么,
歡迎繼續學習AC自動機
首先我們要知道AC自動機是干什么用的。
大家都知道KMP算法是求單字符串對單字符串的匹配使用的
(因為我默認你們上面的兩個東西都學好了)
那么,多個字符串在一個字符串上的匹配怎么辦?
舉例子永遠是最好的
求 abab 在 abababbabbaabbabbab 中出現了幾次
很明顯,求出abab的next數組,然后進行KMP的匹配即可出解。
2. 求 aba aca bab sab sba 在字符串 asabbasbaabbabbacaacbscbs 中總共出現了幾次。 嗯嗯嗯。。。 這個要怎么辦? 每次對一個需要匹配的串求一次next數組,然后一次次去匹配? 顯然,這樣變得很慢很慢了。。。 如果需要匹配的串很多很多的話。。。。。 不敢想象。。,,
那么,我們要如何解決這類問題呢?
恩,AC自動機。
常規而言,看看AC自動機是啥玩意
以下來自某度某科:
Aho-Corasick automation,該算法在1975年產生於貝爾實驗室,是著名的多模匹配算法。
好吧,這個說了跟沒說似的(就象征意義的看一下吧)
正式開始,我們來講解AC自動機
AC自動機需要提前知道所有的需要匹配的字符串
例如
say she shr her
那么第一步
把它們構建成一棵Trie樹

灰色的結點代表一個單詞的結尾
第一步應該是最簡單的一步之一
只需要構建一棵Trie樹即可
(再說一遍,如果前面兩個東西沒學好,先去學習完再繼續學習AC自動機)
接着是第二步,也就是最重要的一步。
構建失配指針
這一步很KMP算法中的next數組是類似的,通過跳轉來省略重復的檢查。
那么要如何構建呢?
我先把構建好的放出來。

恩恩
我知道我畫的圖很丑很丑很丑
並不要在意那些奇怪的顏色問題
雖然畫在了這里,,,,但是
並不知道怎么求對不對。。
我們先看看這個指針是要干什么吧。
每次沿着Trie樹匹配,匹配到當前位置沒有匹配上時,直接跳轉到失配指針所指向的位置繼續進行匹配。
所以,這個Trie樹的失配指針要怎么求?
dalao們的blog告訴我們
Trie樹的失配指針是指向:沿着其父節點 的 失配指針,一直向上,直到找到擁有當前這個字母的子節點 的節點 的那個子節點
感覺聽起來很復雜吧。。。。
我也是這么覺得的
但是
自己畫一下圖就好了
值得一提的是,第二層的所有節點的失配指針,都要直接指向Trie樹的根節點。
我也覺得我自己講的很復雜。。。。。
怎么說呢,求失配指針是一個BFS的過程
需要逐層擴展(因為要用到父節點的失配指針)
所以,覺得每一次求失配指針都需要沿着之前的失配指針走一遍?
顯然並不需要
那么怎么辦?
這里看代碼,自己理解一下(畫圖就是學習AC自動機的訣竅)
void Get_fail()//構造fail指針
{
queue<int> Q;//隊列
for(int i=0;i<26;++i)//第二層的fail指針提前處理一下
{
if(AC[0].vis[i]!=0)
{
AC[AC[0].vis[i]].fail=0;//指向根節點
Q.push(AC[0].vis[i]);//壓入隊列
}
}
while(!Q.empty())//BFS求fail指針
{
int u=Q.front();
Q.pop();
for(int i=0;i<26;++i)//枚舉所有子節點
{
if(AC[u].vis[i]!=0)//存在這個子節點
{
AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i];
//子節點的fail指針指向當前節點的
//fail指針所指向的節點的相同子節點
Q.push(AC[u].vis[i]);//壓入隊列
}
else//不存在這個子節點
AC[u].vis[i]=AC[AC[u].fail].vis[i];
//當前節點的這個子節點指向當
//前節點fail指針的這個子節點
}
}
}
如果你仔細的畫畫圖
就會發現上面是一種很巧妙的構建方式
並不需要沿着失配指針向上移動。
嗷。。。。
失配指針寫完了。。。
最后直接寫匹配???
這個我覺得沒有必要貼代碼
直接講述一下即可
首先,指針指向根節點
依次讀入單詞,檢查是否存在這個子節點
然后指針跳轉到子節點
如果不存在
直接跳轉到失配指針即可
AC自動機差不多就到這里
三個模板題我放一下鏈接,大家可以自己查閱一下完整代碼
upd:2018.3.28
啊,這篇文章是去年暑假寫的
我果斷的更新一下。
重新描述一下關於\(fail\)指針的理解
\(fail\)是失配指針,注意是失配
意味着,如果我此時匹配失敗,那么,我們就要到達這個指針指向的位置繼續常數匹配
所以,我們可以將失配指針指向的的節點理解為:
當前節點所代表的串,最長的、能與后綴匹配的,在\(Trie\)中出現過的前綴所代表的節點。
所以,\(fail\)指針類似於\(kmp\)的\(next\)數組,只不過由單串變為了多串而已。
我們很明顯的看到之前的構建方法是把\(Trie\)樹補全,變成了\(Trie\)圖
雖然很好寫,但並不是試用所有情況下,有的時候需要保留原來的\(Trie\)樹的結構
此時就需要沿着\(fail\)進行尋找學完回文樹之后對這類玩意有感覺多了
所以,我重新提供一個真正的\(AC\)自動機的代碼
這份代碼因為轉移試用了\(map\)記錄,因此有一些\(STL\)的操作
void BuildFail()
{
queue<int> Q;
for(it=t[0].son.begin();it!=t[0].son.end();++it)Q.push(it->second);
while(!Q.empty())
{
int u=Q.front();Q.pop();
if(!t[u].son.size())continue;
for(it=t[u].son.begin();it!=t[u].son.end();++it)
{
int v=it->second,c=it->first,p=t[u].ff;
while(p&&!t[p].son[c])p=t[p].ff;
if(t[p].son[c])t[v].ff=t[p].son[c];
Q.push(v);
t[v].fl|=t[t[v].ff].fl;
}
}
}
