AC自動機

一直想寫AC自動機了
可是考慮到學習AC自動機以前
還須要一點其餘的知識的基礎
因而我先補充好了Trie樹和KMP的blog
若是以上兩個知識點沒有學好的話
請先學習這兩個知識點再來學習AC自動機
Trie(字典樹)
KMP算法算法


若是可以解決上面的兩個 算法/結構 那麼,
歡迎繼續學習AC自動機數組

首先咱們要知道AC自動機是幹什麼用的。學習

你們都知道KMP算法是求單字符串對單字符串的匹配使用的
(由於我默認大家上面的兩個東西都學好了)ui

那麼,多個字符串在一個字符串上的匹配怎麼辦?spa


舉例子永遠是最好的.net

  1. 求 abab 在 abababbabbaabbabbab 中出現了幾回
    很明顯,求出abab的next數組,而後進行KMP的匹配便可出解。


  1. 求 aba aca bab sab sba 在字符串 asabbasbaabbabbacaacbscbs 中總共出現了幾回。
    嗯嗯嗯。。。
    這個要怎麼辦?
    每次對一個須要匹配的串求一次next數組,而後一次次去匹配?
    顯然,這樣變得很慢很慢了。。。
    若是須要匹配的串不少不少的話。。。。。
    不敢想象。。,,

那麼,咱們要如何解決這類問題呢?
恩,AC自動機。指針

常規而言,看看AC自動機是啥玩意
如下來自某度某科:
Aho-Corasick automation,該算法在1975年產生於貝爾實驗室,是著名的多模匹配算法。code

好吧,這個說了跟沒說似的(就象徵意義的看一下吧)blog


正式開始,咱們來說解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自動機差很少就到這裏
三個模板題我放一下連接,你們能夠本身查閱一下完整代碼

【洛谷3808】
【洛谷3796】
【CJOJ1435】


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;
        }
    }
}
相關文章
相關標籤/搜索