AC自動機

版權聲明:本文爲CSDN博主「bestsort」的原創文章,遵循CC 4.0 by-sa版權協議,轉載請附上原文出處連接及本聲明。
原文連接:https://blog.csdn.net/bestsort/article/details/82947639ios

出自bestsort.cn學習

文章非博主原創優化

要學AC自動機須要自備兩個前置技能:KMP和trie樹(其實我的感受不會kmp也行,失配指針的概念並不難)
其中,KMP是用於一對一的字符串匹配,而trie雖然能用於多模式匹配,可是每次匹配失敗都須要進行回溯,若是模式串很長的話會很浪費時間,因此AC自動機應運而生,如同Manacher同樣,AC自動機利用某些操做阻止了模式串匹配階段的回溯,將時間複雜度優化到了O(n) (n)爲文本串長度
下面開始用圖學習ac自動機吧(我的比較喜歡放圖,能用一張圖解決的毫不叨叨)
首先給定模式串"ash","shex","bcd","sha",而後咱們根據模式串創建以下trie樹:spa

而後咱們再瞭解下一步:
ac自動機,就是在tire樹的基礎上,增長一個fail指針,若是當前點匹配失敗,則將指針轉移到fail指針指向的地方,這樣就不用回溯,而能夠路匹配下去了.(當前模式串後綴和fail指針指向的模式串部分前綴相同,如abce和bcd,咱們找到c發現下一個要找的不是e,就跳到bcd中的c處,看看此處的下一個字符(d)是否是應該找的那一個).net

通常,fail指針的構建都是用bfs實現的
首先每一個模式串的首字母確定是指向根節點的(一個字母你瞎指什麼指,指了也是頭字母有什麼用嘛)指針

如今第一層bfs遍歷完了,開始第二層
(根節點爲第0層)第二層a的子節點爲s,可是咱們仍是要從a-z遍歷,若是不存在這個子節點咱們就讓他指向根節點(以下圖紅色的a)code

當咱們遍歷到s的時候,因爲存在s這個節點,咱們就讓他的fail指針指向他父親節點(a)的fail指針指向的那個節點(根)的具備相同字母的子節點(第一層的s),也就是這樣blog

按照相同規律構建第二層後,到了第三層的h點,仍是按照上面的規則,咱們找到h的父親節點(s)fail指針指向的那個位置(第一層的s)而後指向它所指向的相同字母根->s->h的這個鏈的h節點,以下圖隊列

徹底構造好後的樹ci

而後匹配就很簡單了,這裏以ashe爲例
咱們先用ash匹配,到h了發現:誒這裏ash是一個完整的模式串,好的ans++,而後找下一個e,但是ash後面沒字母了啊,咱們就跳到hfail指針指向的那個h繼續找,仍是沒有?再跳,結果當前的h指向的是根節點,又從根節點找,然而仍是沒有找到e,程序END

過程以下圖

喜聞樂見模板系列

#include <queue>
#include <cstdlib>
#include <cmath>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn =  2*1e6+9;

int trie[maxn][26]; //字典樹
int cntword[maxn];  //記錄該單詞出現次數
int fail[maxn];     //失敗時的回溯指針
int cnt = 0;

void insertWords(string s){
    int root = 0;
    for(int i=0;i<s.size();i++){
        int next = s[i] - 'a';
        if(!trie[root][next])
            trie[root][next] = ++cnt;
        root = trie[root][next];
    }
    cntword[root]++;      //當前節點單詞數+1
}
void getFail(){
    queue <int>q;
    for(int i=0;i<26;i++){      //將第二層全部出現了的字母扔進隊列
        if(trie[0][i]){
            fail[trie[0][i]] = 0;
            q.push(trie[0][i]);
        }
    }

//fail[now]    ->當前節點now的失敗指針指向的地方
////tire[now][i] -> 下一個字母爲i+'a'的節點的下標爲tire[now][i]
    while(!q.empty()){
        int now = q.front();
        q.pop();

        for(int i=0;i<26;i++){      //查詢26個字母
            if(trie[now][i]){
                //若是有這個子節點爲字母i+'a',則
//讓這個節點的失敗指針指向(((他父親節點)的失敗指針所指向的那個節點)的下一個節點)
                //有點繞,爲了方便理解特地加了括號

                fail[trie[now][i]] = trie[fail[now]][i];
                q.push(trie[now][i]);
            }
            else//不然就讓當前節點的這個子節點
                //指向當前節點fail指針的這個子節點
                trie[now][i] = trie[fail[now]][i];
        }
    }
}


int query(string s){
    int now = 0,ans = 0;
    for(int i=0;i<s.size();i++){    //遍歷文本串
        now = trie[now][s[i]-'a'];  //從s[i]點開始尋找
        for(int j=now;j && cntword[j]!=-1;j=fail[j]){
            //一直向下尋找,直到匹配失敗(失敗指針指向根或者當前節點已找過).
            ans += cntword[j];
            cntword[j] = -1;    //將遍歷國後的節點標記,防止重複計算
        }
    }
    return ans;
}

int main() {
    int n;
    string s;
    cin >> n;
    for(int i=0;i<n;i++){
        cin >> s ;
        insertWords(s);
    }
    fail[0] = 0;
    getFail();
    cin >> s ;
    cout << query(s) << endl;
    return 0;
}