AC自動機算法詳解 (轉載)

AC自動機算法詳解 (轉載)

https://www.cnblogs.com/cmmdc/p/7337611.html

首先簡要介紹一下AC自動機:Aho-Corasick automation,該算法在1975年產生于貝爾實驗室,是著名的多模匹配算法之一。一個常見的例子就是給出n個單詞,再給出一段包含m個字符的文章,讓你找出有多少個單詞在文章裏出現過。要搞懂AC自動機,先得有模式樹(字典樹)Trie和KMP模式匹配算法的基礎知識。KMP算法是單模式串的字符匹配算法,AC自動機是多模式串的字符匹配算法。

AC自動機和字典樹的關係比較大,所以先來簡單的瞭解下字典樹Trie。

字典樹又稱單詞查找樹,Trie樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,查詢效率比哈希樹高。

簡而言之:字典樹就是像平時使用的字典一樣的,我們把所有的單詞編排入一個字典裏面,當我們查找單詞的時候,我們首先看單詞首字母,進入首字母所再的樹枝,然後看第二個字母,再進入相應的樹枝,假如該單詞再字典樹中存在,那麼我們只用花費單詞長度的時間查詢到這個單詞。

AC自動機關鍵點一:字典樹的構建過程:

字典樹的構建過程是這樣的,當要插入許多單詞的時候,我們要從前往後遍歷整個字符串,當我們發現當前要插入的字符其節點再先前已經建成,我們直接去考慮下一個字符即可,當我們發現當前要插入的字符沒有再其前一個字符所形成的樹下沒有自己的節點,我們就要創建一個新節點來表示這個字符,接下往下遍歷其他的字符。然後重複上述操作。

假設我們有下面的單詞,she , he ,say, her, shr ,我們要構建一棵字典樹

AC自動機關鍵點二:找Fail指針

在KMP算法中,當我們比較到一個字符發現失配的時候我們會通過next數組,找到下一個開始匹配的位置,然後進行字符串匹配,當然KMP算法試用與單模式匹配,所謂單模式匹配,就是給出一個模式串,給出一個文本串,然後看模式串在文本串中是否存在。

在AC自動機中,我們也有類似next數組的東西就是fail指針,當發現失配的字符失配的時候,跳轉到fail指針指向的位置,然後再次進行匹配操作,AC自動機之所以能實現多模式匹配,就歸功於Fail指針的建立。

當前節點t有fail指針,其fail指針所指向的節點和t所代表的字符是相同的。因爲t匹配成功後,我們需要去匹配t->child,發現失配,那麼就從t->fail這個節點開始再次去進行匹配。

Fail指針的求法:
Fail指針用BFS來求得,對於直接與根節點相連的節點來說,如果這些節點失配,他們的Fail指針直接指向root即可,其他節點其Fail指針求法如下:
假設當前節點爲father,其孩子節點記爲child。求child的Fail指針時,首先我們要找到其father的Fail指針所指向的節點,假如是t的話,我們就要看t的孩子中有沒有和child節點所表示的字母相同的節點,如果有的話,這個節點就是child的fail指針,如果發現沒有,則需要找father->fail->fail這個節點,然後重複上面過程,如果一直找都找不到,則child的Fail指針就要指向root。

如圖所示,首先root最初會進隊,然後root,出隊,我們把root的孩子的失敗指針都指向root。因此圖中h,s的失敗指針都指向root,如紅色線條所示,同時h,s進隊。

接下來該h出隊,我們就找h的孩子的fail指針,首先我們發現h這個節點其fail指針指向root,而root又沒有字符爲e的孩子,則e的fail指針是空的,如果爲空,則也要指向root,如圖中藍色線所示。並且e進隊,此時s要出隊,我們再找s的孩子a,h的fail指針,
我們發現s的fail指針指向root,而root沒有字符爲a的孩子,故a的
fail指針指向root,a入隊,然後找h的fail指針,同樣的先看s的fail指針是root,發現root又字符爲h的孩子,所以h的fail指針就指向了第二層的h節點。e,a , h 的fail指針的指向如圖藍色線所示。

此時隊列中有e,a,h,e先出隊,找e的孩子r的失敗指針,我們先看e的失敗指針,發現找到了root,root沒有字符爲r
的孩子,則r的失敗指針指向了root,並且r進隊,然後a出隊,我們也是先看a的失敗指針,發現是root,則y的fail指針就會指向root.並且y進隊。然後h出隊,考慮h的孩子e,則我們看h的失敗指針,指向第二層的h節點,看這個節點發現有字符值爲e的節點,最後一行的節點e的失敗指針就指向第三層的e。最後找r的指針,同樣看第二層的h節點,其孩子節點不含有字符r,則會繼續往前找h的失敗指針找到了根,根下面的孩子節點也不存在有字符r,則最後r就指向根節點,最後一行節點的fail指針如綠色虛線所示。

AC自動機關鍵點三:文本串的匹配

匹配過程分兩種情況:
(1)當前字符匹配,表示從當前節點沿着樹邊有一條路徑可以到達目標字符,如果當前匹配的字符是一個單詞的結尾,我們可以沿着當前字符的fail指針,一直遍歷到根,如果這些節點末尾有標記(此處標記代表,節點是一個單詞末尾的標記),這些節點全都是可以匹配上的節點。我們統計完畢後,並將那些節點標記。此時只需沿該路徑走向下一個節點繼續匹配即可,目標字符串指針移向下個字符繼續匹配;
(2)當前字符不匹配,則去當前節點失敗指針所指向的字符繼續匹配,匹配過程隨着指針指向root結束。重複這2個過程中的任意一個,直到模式串走到結尾爲止。

對照上圖,看一下模式匹配這個詳細的流程,其中模式串爲yasherhs。對於i=0,1。Trie中沒有對應的路徑,故不做任何操作;i=2,3,4時,指針p走到左下節點e。因爲節點e的count信息爲1,所以cnt+1,並且講節點e的count值設置爲-1,表示改單詞已經出現過了,防止重複計數,最後temp指向e節點的失敗指針所指向的節點繼續查找,以此類推,最後temp指向root,退出while循環,這個過程中count增加了2。表示找到了2個單詞she和he。當i=5時,程序進入第5行,p指向其失敗指針的節點,也就是右邊那個e節點,隨後在第6行指向r節點,r節點的count值爲1,從而count+1,循環直到temp指向root爲止。最後i=6,7時,找不到任何匹配,匹配過程結束。

AC自動機算法分爲3步:構造一棵Trie樹,構造失敗指針和模式匹配過程。
如果你對KMP算法和了解的話,應該知道KMP算法中的next函數(shift函數或者fail函數)是幹什麼用的。KMP中我們用兩個指針i和j分別表示,A[i-j+ 1..i]與B[1..j]完全相等。也就是說,i是不斷增加的,隨着i的增加j相應地變化,且j滿足以A[i]結尾的長度爲j的字符串正好匹配B串的前 j個字符,當A[i+1]≠B[j+1],KMP的策略是調整j的位置(減小j值)使得A[i-j+1..i]與B[1..j]保持匹配且新的B[j+1]恰好與A[i+1]匹配,而next函數恰恰記錄了這個j應該調整到的位置。同樣AC自動機的失敗指針具有同樣的功能,也就是說當我們的模式串在Tire上進行匹配時,如果與當前節點的關鍵字不能繼續匹配的時候,就應該去當前節點的失敗指針所指向的節點繼續進行匹配。

  看下面這個例子:給定5個單詞:say she shr he her,然後給定一個字符串yasherhs。問一共有多少單詞在這個字符串中出現過。我們先規定一下AC自動機所需要的一些數據結構,方便接下去的編程。
const int kind = 26; 
struct node
{
    node *fail;       //失敗指針
    node *next[kind]; //Tire每個節點的個子節點(最多個字母)
    int count;        //是否爲該單詞的最後一個節點
    node()            //構造函數初始化
    {
        fail=NULL;
        count=0;
        memset(next,NULL,sizeof(next));
    }
}*q[500001];          //隊列,方便用於bfs構造失敗指針
char keyword[51];     //輸入的單詞
char str[1000001];    //模式串
int head,tail;        //隊列的頭尾指針

有了這些數據結構之後,就可以開始編程了:
首先,將這5個單詞構造成一棵Tire,如圖-1所示。

void insert(char *str,node *root){ 
    node *p=root;
    int i=0,index;
    while(str[i])
    {
        index=str[i]-'a';
        if(p->next[index]==NULL) p->next[index]=new node();
        p=p->next[index];
        i++;
    }
    p->count++;     //在單詞的最後一個節點count+1,代表一個單詞
}

 

在構造完這棵Tire之後,接下去的工作就是構造下失敗指針。構造失敗指針的過程概括起來就一句話:設這個節點上的字母爲C,沿着他父親的失敗指針走,直到走到一個節點,他的兒子中也有字母爲C的節點。然後把當前節點的失敗指針指向那個字母也爲C的兒子。如果一直走到了root都沒找到,那就把失敗指針指向root。具體操作起來只需要:先把root加入隊列(root的失敗指針指向自己或者NULL),這以後我們每處理一個點,就把它的所有兒子加入隊列,隊列爲空。

 void build_ac_automation(node *root){
    int i;
    root->fail=NULL;
    q[head++]=root;
    while(head!=tail)
    {
        node *temp=q[tail++];
        node *p=NULL;
        for(i=0; i<26; i++)
        {
            if(temp->next[i]!=NULL)
            {
                if(temp==root) temp->next[i]->fail=root;
                else
                {
                    p=temp->fail;
                    while(p!=NULL)
                    {
                        if(p->next[i]!=NULL)
                        {
                            temp->next[i]->fail=p->next[i];
                            break;
                        }
                        p=p->fail;
                    }
                    if(p==NULL) temp->next[i]->fail=root;
                }
                q[head++]=temp->next[i];
            }
        }
    }
}

從代碼觀察下構造失敗指針的流程:對照圖-2來看,首先root的fail指針指向NULL,然後root入隊,進入循環。第1次循環的時候,我們需要處理2個節點:root->next‘h’-‘a’ 和 root->next‘s’-‘a’。把這2個節點的失敗指針指向root,並且先後進入隊列,失敗指針的指向對應圖-2中的(1),(2)兩條虛線;第2次進入循環後,從隊列中先彈出h,接下來p指向h節點的fail指針指向的節點,也就是root;進入第13行的循環後,p=p->fail也就是p=NULL,這時退出循環,並把節點e的fail指針指向root,對應圖-2中的(3),然後節點e進入隊列;第3次循環時,彈出的第一個節點a的操作與上一步操作的節點e相同,把a的fail指針指向root,對應圖-2中的(4),併入隊;第4次進入循環時,彈出節點h(圖中左邊那個),這時操作略有不同。在程序運行到14行時,由於p->next[i]!=NULL(root有h這個兒子節點,圖中右邊那個),這樣便把左邊那個h節點的失敗指針指向右邊那個root的兒子節點h,對應圖-2中的(5),然後h入隊。以此類推:在循環結束後,所有的失敗指針就是圖-2中的這種形式。

最後,我們便可以在AC自動機上查找模式串中出現過哪些單詞了。匹配過程分兩種情況:(1)當前字符匹配,表示從當前節點沿着樹邊有一條路徑可以到達目標字符,此時只需沿該路徑走向下一個節點繼續匹配即可,目標字符串指針移向下個字符繼續匹配;(2)當前字符不匹配,則去當前節點失敗指針所指向的字符繼續匹配,匹配過程隨着指針指向root結束。重複這2個過程中的任意一個,直到模式串走到結尾爲止。

 int query(node *root){ 
    int i=0,cnt=0,index,len=strlen(str);
    node *p=root;
    while(str[i])
    {
        index=str[i]-'a';
        while(p->next[index]==NULL && p!=root) p=p->fail;
        p=p->next[index];
        p=(p==NULL)?root:p;
        node *temp=p;
        while(temp!=root && temp->count!=-1)
        {
            cnt+=temp->count;
            temp->count=-1;
            temp=temp->fail;
        }
        i++;
    }
    return cnt;
}

對照圖-2,看一下模式匹配這個詳細的流程,其中模式串爲yasherhs。對於i=0,1。Trie中沒有對應的路徑,故不做任何操作;i=2,3,4時,指針p走到左下節點e。因爲節點e的count信息爲1,所以cnt+1,並且講節點e的count值設置爲-1,表示改單詞已經出現過了,防止重複計數,最後temp指向e節點的失敗指針所指向的節點繼續查找,以此類推,最後temp指向root,退出while循環,這個過程中count增加了2。表示找到了2個單詞she和he。當i=5時,程序進入第5行,p指向其失敗指針的節點,也就是右邊那個e節點,隨後在第6行指向r節點,r節點的count值爲1,從而count+1,循環直到temp指向root爲止。最後i=6,7時,找不到任何匹配,匹配過程結束。

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