淺談AC自動機

0.前言

第一眼看,還以爲是自動AC機,學了就可以ak虐全場了。
可惜理想很豐滿,現實卻很骨感,它叫AC自動機,是一種字符串算法
我們都知道KMP算法是可以在一個文本串中找到你想找的一個模式串的位置的,
而AC自動機的主要用處就在於可以把多個模式串匹配到文本串中,並且可以找到一個模式串出現的所有位置,好高級呀。
預備知識:
trie樹就夠了

一.思想

我們先對所有需要匹配的模式串建立trie樹,
然後要建立fail指針,指向與當前後綴相等的前綴,來一張圖幫助理解:
在這裏插入圖片描述
虛線就是我們的fail指針,首先所有開頭的字母都指向根。然後,圖中h指向另一個子樹的h是因爲另一個字符串的前綴sh與我當前的後綴sh一樣,其他的指針同理。
有了這個東西,當我們匹配不下去的時候,就可以轉向fail指針指向的那個節點繼續查找,這個時間複雜度就很低。
這就是AC自動機的主要思想。
時間複雜度:O(n)O(n)

二.實現

1.建樹

建trie樹就不用我說了吧

void insert (char *a){
    int len = strlen (a), now = 0;
    for (int i = 0; i < len; i ++){
        if (! tire[now][a[i] - 'a'])
            tire[now][a[i] - 'a'] = ++ cnt;
        now = tire[now][a[i] - 'a'];
    }
    cntword[now] ++;//記錄當前節點有多少個完整的模式串
}

2.預處理fail

這裏我們要用bfs,搜索這顆trie樹
注意:這個節點的fail要麼等於它父親節點的fail的後一個,要麼就是0.

void getfail (){
    queue <int> Q;
    for (int i = 0; i < 26; i ++){//先把所有模式串的開頭扔進隊列
        if (tire[0][i]){
            fail[tire[0][i]] = 0;//指向根
            Q.push (tire[0][i]);
        }
    }
    while (! Q.empty ()){//bfs
        int f = Q.front ();
        Q.pop ();
        for (int i = 0; i < 26; i ++){
            if (tire[f][i]){
                fail[tire[f][i]] = tire[fail[f]][i];//這個節點的fail等於它父親節點的fail的後一個
                Q.push (tire[f][i]);
            }
            else
                tire[f][i] = tire[fail[f]][i];
        }
    }
}

3.AC自動機的操作

int ACzdj (char *a){
    int len = strlen (a), now = 0, ans = 0;
    for (int i = 0; i < len; i ++){//遍歷文本串每一個字符
        now = tire[now][a[i] - 'a'];
        for (int j = now; j && cntword[j] != -1; j = fail[j]){//找到所有的相同前綴去查找單詞數量
            ans += cntword[j];
            cntword[j] = -1;//找到一個節點就附一個-1
        }
    }
    return ans;
}

三.模板

這道模板題:AC自動機
Code:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;

#define M 1000005

int n, tire[M][30], cnt, cntword[M], fail[M];

void insert (char *a){
    int len = strlen (a), now = 0;
    for (int i = 0; i < len; i ++){
        if (! tire[now][a[i] - 'a'])
            tire[now][a[i] - 'a'] = ++ cnt;
        now = tire[now][a[i] - 'a'];
    }
    cntword[now] ++;
}
void getfail (){
    queue <int> Q;
    for (int i = 0; i < 26; i ++){
        if (tire[0][i]){
            fail[tire[0][i]] = 0;
            Q.push (tire[0][i]);
        }
    }
    while (! Q.empty ()){
        int f = Q.front ();
        Q.pop ();
        for (int i = 0; i < 26; i ++){
            if (tire[f][i]){
                fail[tire[f][i]] = tire[fail[f]][i];
                Q.push (tire[f][i]);
            }
            else
                tire[f][i] = tire[fail[f]][i];
        }
    }
}
int ACzdj (char *a){
    int len = strlen (a), now = 0, ans = 0;
    for (int i = 0; i < len; i ++){
        now = tire[now][a[i] - 'a'];
        for (int j = now; j && cntword[j] != -1; j = fail[j]){
            ans += cntword[j];
            cntword[j] = -1;
        }
    }
    return ans;
}
int main (){
    char a[M];
    scanf ("%d", &n);
    for (int i = 1; i <= n; i ++){
        scanf ("%s", a);
        insert (a);
    }
    getfail();
    scanf ("%s", a);
    printf ("%d\n", ACzdj (a));
    return 0;
}

謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章