1. 程式人生 > >演算法學習——AC自動機

演算法學習——AC自動機

其實算是複習了。。。

 首先在學習這個之前我們需要學會trie樹這個東西,詳見演算法複習——trie樹

  AC自動機就是在trie樹的基礎上建立起來的。

  先看幾個定義:

    1,fail指標:這個指標指向 滿足等於當前串的某一字尾的串中,深度最大的那個串。因為在trie樹上任意一個點都可以代表從root到這個點所構成的串,所以假設我們現在有abcd這個串,這個指標指向的串是bcd,這就是合法的。因為bcd這個串是abcd這個串的某一字尾。

    2,fail樹:我們把每個節點的fail指標看做邊,建出一棵樹,這棵樹被稱為fail樹。

  fail指標有什麼用呢?

    1,首先我們可以發現,如果我們要求在一個串S中有哪些字串出現過的話,我們可以發現如果一個串x在S中出現過,那麼那個串的fail指向的串也必定在S中出現過。

    2,所以我們可以直接在trie樹上遍歷S串,如果遇到了一個沒有的節點,我們就回到root,否則下一步去它的兒子 or fail(在沒有兒子的情況下才去fail)

    因為第1點,所以每當我們遍歷到一個串時,我們就需要沿著它的fail樹上的邊(即fail指標)向上遍歷,不斷累加貢獻。

  那麼如何求fail呢?

    首先對於root的兒子而言,它們的fail肯定都是root,因為沒有更短的串了QAQ。

    然後我們可以用類似DP的方式來求fail陣列。

    首先我們可以觀察到一個點的fail,一定是沿著它的父親的fail向上遍歷遇到的某個點的兒子。因為它父親的fail已經滿足最長這個限制了,那麼我們肯定要在這個基礎上儘量擴充套件。

    所以難道每次更新的時候我們都要暴跳fail樹嗎?

    其實是不需要的,因為fail肯定會指向深度更淺的點,所以我們用bfs的方式來遍歷trie樹。然後我們有一個巧妙的寫法來實現O(1)的轉移。

1         while(head<tail)
2         {
3             now=q[++head];
4             for(R i=0
;i<26;i++) 5 if(c[now][i]) fail[c[now][i]]=c[fail[now]][i],q[++tail]=c[now][i]; 6 else c[now][i]=c[fail[now]][i];//建立虛擬節點 7 }

    即每次當前節點的某個兒子為空的時候,我們強行把這個空兒子指向它fail的這個兒子,這樣下次有點再需要判斷這個空兒子是否存在時,如果它存在,就可以直接指向它,如果它不存在,就會被指向它再往上跳一步的那個位置的兒子。(注意也不一定就是跳1步,也可能是跳很多步,因為c[fail[now]][i]這個節點也可能就是記錄了向上跳x步的一個位置,這樣一步步累計下來就可能有很多步了)

    可以畫圖理解一下。

  

  但我們可以注意到一點,當我們建出AC自動機後,我們在AC自動機上的每一步操作,其實質都是在fail樹上進行操作。

  而如果我們匹配文字串時每次都一步一步的跳fail,是可能退化到$n^2$複雜度的(在需要重複統計的情況下,類似[TJOI2013]單詞,即每個串可以造成多次貢獻)。

  這個時候可以分2種情況討論:

  1,如果我們只需要記錄總的貢獻,我們可以對fail樹上的每個節點做個記憶化,因為一旦訪問到一個節點,接下來會怎麼走都是固定的(父親只有一個)。所以我們可以在訪問過某個節點後記錄下一旦我們訪問到這個節點,我們可以得到多少貢獻,然後下一次再訪問到這個節點就不用再往上跳了,可以直接退出了。

  2,如果我們需要對每個小串分別記錄貢獻,即貢獻是加在小串上面的。那麼因為我們遍歷到一個點,其實就是要給這個點到root的這條鏈上的所有點的ans都++,所以我們可以直接給這個點打上加1的標記,然後用類似O(n)求樹狀陣列的方法在最後統計貢獻。當然也可以這麼考慮,因為一個點上的標記可以對當前點產生貢獻當且僅當那個點是當前點的子樹,所以想辦法維護一下子樹和之類的。

  不過更好寫的方法是在最後用類似O(n)求樹狀陣列的方法統計貢獻。

  大致方法如下:

    對於一個節點x,如果我們把它的標記傳給fa[x],然後我們再把fa[x]的標記傳給fa[fa[x]],我們就可以看做fa[x]在上傳貢獻的時候順便把x捎給它的貢獻也傳上去了,相當於一個人傳了2份貢獻,其中一份屬於x,一份屬於fa[x].

    那麼要讓fa[x]順便把x的貢獻也傳上去的條件就是x要在fa[x]上傳貢獻之前就把貢獻傳給fa[x].

    那麼顯然我們只需要用拓撲序的順序來上傳貢獻就可以保證這點了。

    又因為我們建fail樹的時候是用的bfs,bfs的佇列裡面其實就是一個拓撲序,所以就可以直接呼叫原來用過的陣列,而不用再求拓撲序。

    程式碼為luogu上面AC自動機(簡單版)的模板。

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 #define R register int
 4 #define AC 500100
 5 #define ac 1001000
 6 
 7 int n, len, ans;
 8 int q[ac], head, tail;
 9 char s[ac];
10 
11 struct AC_machine{
12     int c[AC][26], fail[AC], val[AC], tot;
13         
14     inline void ins()//插入一個字串
15     {
16         int now = 0;
17         for(R i = 1; i <= len; i ++)
18         {
19             int v = s[i] - 'a';
20             if(!c[now][v]) c[now][v] = ++ tot;
21             now = c[now][v];
22         }
23         ++ val[now];
24     }
25     
26     void build()
27     {
28         for(R i = 0; i < 26; i ++) 
29             if(c[0][i]) q[++ tail] = c[0][i];
30         int now;
31         while(head < tail)
32         {
33             now = q[++ head];
34             for(R i = 0; i < 26; i ++)
35             {
36                 if(c[now][i]) fail[c[now][i]] = c[fail[now]][i], q[++ tail] = c[now][i];
37                 else c[now][i] = c[fail[now]][i];
38             } 
39         }
40     }
41     
42     void get()
43     {
44         int now = 0;
45         for(R i = 1; i <= len; i ++)
46         {
47             now = c[now][s[i] - 'a'];
48             for(R i = now; i && ~val[i]; i = fail[i]) ans += val[i], val[i] = -1; 
49         }
50         printf("%d\n", ans);
51     }
52 }T;
53 
54 void pre()
55 {
56     scanf("%d", &n);
57     for(R i = 1; i <= n; i ++) 
58         scanf("%s", s + 1), len = strlen(s + 1), T.ins();
59     scanf("%s", s + 1), len = strlen(s + 1);
60 }
61 
62 int main()
63 {
64 //    freopen("in.in", "r", stdin);
65     pre();
66     T.build();
67     T.get();
68 //    fclose(stdin);
69     return 0;
70 }
View Code