AC自動機詳解 (P3808 模板)


AC自動機筆記

0.0 前言

哇,好久之前就看了 KMP 和 Trie 樹,但是似乎一直沒看懂 AC自動機??
今天靈光一閃,加上之前看到一些博客和視頻,瞬間秒懂啊... 其實這個玩意還是蠻好理解的...

在這里先給一個樣例,之后也都好舉例子.

模式串:
5
FG
HE
HERS
HIS
SHE

匹配串:HISHERS

1.1 深度理解 KMP

KMP 算法的精華部分即其處理的 Next 數組.
Next 數組所存的數值即代表j之前的字符串中有最大長度為k 的相同前后綴.
記錄這個有什么用呢?
對於ABCDABC這個串,如果我們匹配ABCDABTBCDABC這個長串,當匹配到第7個字符T的時候就不匹配了,我們就不用直接移到B開始再比一次,而是直接移到第5位來比較,豈不美哉?
Next 數組存的是最長的相同的串長度,再從第一個開始匹配明顯是不需要的,我的最長公共前后綴的位置即為第一個可能匹配成功的位置.
所以其正確性很明顯,而 KMP 其中的失配匹配思想 (姑且讓我這么說說罷) 是 AC自動機中很重要的一環.

1.2 利用 Trie 樹

關於AC自動機中的 Trie 樹,其實其最大做用就是用來存儲給出的字典.
Trie 樹在處理很多查找問題時都會有不錯的效用.01 Trie 樹也是處理異或問題的一大利器.
利用 Trie 樹,我們對上文中的例子就可以構建出一張圖:
(先不要管點上的數字,度娘的圖...虛線后面會用到)

2.0 Trie 樹上的 KMP

以上二者即為 AC自動機 的前置技能.
我們需要建立一棵 Trie 樹,用於存儲其給出的字典.
同時我們需要利用 KMP 其中的失配匹配思想,用以優化時間復雜度.

2.1 Fail 指針


概念

AC 自動機中的 Fail 指針,其實就相當於 KMP 中的 Next 數組.
我們先看看 Fail 是怎么跳的.
然后再一次搬出上文那張圖:

此時我們便可以對圖中的虛線作出解釋了.
每個點所對應虛線指向的地方,即為這個點的 Fail 指針.

Fail 指針指向的地方是與 從Trie 樹起點到這個點 公共前后綴最長的地方.
同時要求這個前后綴不能與其等長(否則不就是自己么?).
比如說 90 這個節點, 從起點到這個點所形成的串即為 S H.
然后我們找到另外一個與其公共前后綴最長的點,就是 74 了.
此時它們的公共前后綴即為 H.
又如 S H EH E 這兩個位置.

匹配過程

然后此時我們看一下怎么匹配所給出的匹配串.

H I S H E R S

先匹配 H 然后走進 74 這個節點.
繼續匹配 I 然后走進 80 這個節點.
繼續匹配 S 然后發現這里有一個單詞,於是統計答案.

統計到 S 后,我們下一個看往的方向即為它的 Fail -- 85.
因為它們有公共前后綴 S.

然后發現 85 的后一個正好即為 H,然后我們與之匹配.
再匹配 90和91. 然后我在 91 統計答案.

因為后面已經無路可走,於是我們跳到 91 的 Fail,即為 76.
它們有公共前后綴 H E.

然后我們繼續跑到 86 ,此時需要被匹配的串已經走完了,我們整個過程結束.

2.2 Fail 的尋找

可以看出,當我們 Fail 找完之后,整個 AC 自動機的工作其實已經完成了一大半.那么我們如何處理出 Fail 指針呢?
我們用廣搜實現.

首先對於根節點的子節點,因為只有一個字母,所以不可能有其余的子串與其有公共前后綴,所以直接會根節點.

然后對於接下來的 Fail ,我們把它直接賦為它爸爸的Fail 子節點中與其字母相等的子節點. 上一小段代碼:

  for(int i=0;i<26;i++)
        if(ch[u][i])
            {
                f[ch[u][i]]=ch[f[u]][i];
                q.push(ch[u][i]);
            }

2.3 Code

/*
Problem : AC自動機
Time : Day -94
*/
#include<bits/stdc++.h>
#define maxn 1000008
using namespace std;

struct AC_machine
{
    int ch[maxn][26];
    int num[maxn],f[maxn];
    // f即為fail指針.
    int tot;
    void insert(string s)
    {
        int u=0,len=s.length();
        for(int i=0;i<len;i++)
        {
            if(!ch[u][s[i]-'a'])
                ch[u][s[i]-'a']=++tot;
            u=ch[u][s[i]-'a'];
        }
        num[u]++;
    } //往Trie樹里插入元素.
    void build()
    {
        queue<int> q;
        for(int i=0;i<26;i++)
        {
            if(ch[0][i])
            f[ch[0][i]]=0,
            //第一層與其他單詞不可能有公共前后綴,fail直接為根.
            q.push(ch[0][i]);
        }
        while(q.empty()!=1)
        {
            int u=q.front(); q.pop();
            for(int i=0;i<26;i++)
            if(ch[u][i])
            {
                f[ch[u][i]]=ch[f[u]][i];
                q.push(ch[u][i]);
                //畫圖理解賊容易.
            }
            else ch[u][i]=ch[f[u]][i];
            //這一步直接省略了查詢時的比較.
        }
    } //構建Fail指針.
    int query(string s) 
    {
    	int u=0,len=s.length(),ans = 0;
    	for(int i=0;i<len;i++)
    	{
        	u=ch[u][s[i]-'a'];
        	for(int j=u;j&&num[j]!=-1;j=f[j]) 
        	//就用這個循環實現跳的過程.
        	ans+=num[j],num[j]=-1;
        	//因為直接已經在每個單詞的最后面打了標記,所以直接加上即可.
    	}
    return ans;
    }
}AC;
int n;string s;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        AC.insert(s);
    }
    AC.build();
    cin>>s;
    cout<<AC.query(s)<<endl;
}


幾個常用的優化

1.類路徑壓縮(構建 Fail 時優化)
詳見以下代碼:

if(ch[u][i])
            {
                f[ch[u][i]]=ch[f[u]][i];
                q.push(ch[u][i]);
            }
            else ch[u][i]=ch[f[u]][i];

此步操作使得在比較時省去了 While 循環.
如果沒有匹配,直接進入 Fail 的匹配之中.
(參考劉汝佳《算法競賽入門經典訓練指南》P216).


2.后綴鏈接
這里我們需要多加一個 last 數組.
同樣見代碼:

  • 構建 Fail 部分
if(ch[u][i])
            {
                f[ch[u][i]]=ch[f[u]][i];
                q.push(ch[u][i]);
                last[u]=num[f[u]]?f[u]:last[f[p]]
            }
            else ch[u][i]=ch[f[u]][i];
  • 匹配查詢過程
for(int i=0;i<len;i++)
    	{
        	u=ch[u][s[i]-'a'];
        	for(int j=u;j&&num[j]!=-1;j=last[j]) 
        	ans+=num[j],num[j]=-1;
    	}

此處我們的 last 大概可以理解為一個 超級 Fail.
因為我們只有到根節點時才會重新匹配一個字母
所以我們此時直接記錄一個last ,直接結束當前匹配過程.
直接省去原 Fail 指針到可以匹配的節點之間的距離.
同時結合上文類路徑壓縮,在匹配時可以完全不使用原 Fail.


3.樹形DP優化
此處樹形DP優化的是查詢部分.
首先我們可以發現, Fail 指針是絕對滿足樹形結構的.
顯而易見,每個點的 Fail 都僅指向一個一個節點.
然后具體做的過程我似乎還沒學懂...
不過也可以理解為構建一個 超級 Fail,優化的部分與 2 差不多.

小結

AC自動機算法的精華在於Fail 所體現的失配匹配思想.
在 KMP 中也都有體現,在不同的題目中,也應巧妙運用這一性質.









參考(講的都比我好QwQ):


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM