算法總結篇---AC自動機

寫在前面

鳴謝:
OiWiki
「筆記」AC 自動機---LuckyBlock
字符串四姐妹---老色批
AC自動機講解超詳細---某不知名大佬html

Q:AC自動機?是能本身AC題目的算法嗎?(興奮)
A:不不不,那叫自動AC機,經過打開答案文件輸出答案的一種小手段,在比賽中使用還會有禁賽三年的獎勵,而AC自動機是一個字符串匹配算法算法

AC自動機,全稱\(Aho-Corasick\ automaton\),是一種用來處理字符串多模式匹配的算法函數

本人將盡量詳細的解釋AC自動機的算法流程(其實大部分抄的Oiwiki,這是一個幫助咱們共同理解的過程,畢竟做者也是個萌新。開始接受的過程可能比較困難,但多回顧幾遍仍是有助於理解的優化


算法流程


前置知識:Trie樹以及KMP算法的思想ui

什麼是自動機?(粘個連接,感性理解就好,不要過於執着)spa


引例:

給定 \(n\) 個模式串 \(s_i\) 和一個文本串 \(t\),求有多少個不一樣的模式串在文本串裏出現過。
兩個模式串不一樣當且僅當他們編號不一樣。指針

概述:

結合Trie的結構KMP的思想創建,創建一個AC自動機主要經過兩個步驟:code

  • 一、創建Trie樹;htm

  • 二、對Trie樹上的全部結點構造失配指針blog

Trie樹的構建(第一步)

這個Trie樹就是普通的Trie樹,該怎麼建怎麼建

解釋一下Trie樹結點的含義:表示某個模式串的前綴
後文也將稱做狀態。一個結點表示一個狀態,Trie樹的邊就是狀態的轉移

形式化的說,對於若干個模式串 \(s_1,s_2,s_3···s_n\),將它們構建一個Trie樹後的全部狀態的集合記爲 \(Q\)

失配指針(第二步)

AC 自動機利用一個 fail 指針來輔助多模式串的匹配。

狀態 \(u\) 的 fail 指針指向另外一個狀態 \(v\) ,其中 \(v \in Q\) ,且 \(v\)\(u\) 的最長後綴(即在若干個後綴狀態中取最長的一個做爲 fail 指針)。

注意和KMP的next指針的區別:

二者都是在失配的時候用於跳轉的指針;
next指針求的是最長的border(最長的 相同的 先後綴),而fail指針指向全部模式串的前綴中匹配當前狀態的最長後綴

由於 KMP 只對一個模式串作匹配,而 AC 自動機要對多個模式串作匹配。有可能 fail 指針指向的結點對應着另外一個模式串,二者前綴不一樣。

AC 自動機在作匹配時,同一位上可匹配多個模式串。

構建失配指針

(能夠參考KMP中構建next指針的思想(

考慮更新 \(fail_u\)\(u\) 的父節點是 \(p\) , \(p\) 經過字符 \(c\) 的邊指向 \(u\) ,即 \(tr[p,c] = u\) 。假設深度小於 \(u\) 的全部結點的 \(fail\) 指針均已求得。

若是 \(tr[fail_p,c]\) 存在:則讓 \(fail_u\) 指向 \(tr[fail[p],c]\) 。至關於在 \(p\)\(fail\) 後面加一個字符 c ,分別對應 \(u\)\(fail_u\)
若是 \(tr[fail_p,c]\) 不存在:那麼咱們繼續找到 \(tr[fail_{fail_p},c],c]\) 。重複 \(1\) 的判斷過程,一直跳 \(fail_u\) 指針指到根結點。
若是真的沒有,就讓 \(fail_u\) 指針指向根結點。

這樣就完成了 \(fail\) 的構建,並獲得一份比較暴力的構建方式,咱們來看優化

字典樹和字典圖

先來看構建函數 build() ,該函數的目標有兩個,一個是構建 fail 指針,一個是構建自動機。

void build(){
		for(int i = 0; i < 26; ++i) if(tr[0][i]) q.push(tr[0][i]);
		//若是存在這個邊就入隊
		while(!q.empty()){
			int u = q.front(); q.pop();
			for(int i = 0; i < 26; ++i){
				if(tr[u][i]) fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
				//按照上面所說的方式更新fail指針
				else tr[u][i] = tr[fail[u]][i];//這是那個優化,後面會講
			}
		}
	}

原來的構建方法能夠經過 \(while\) 循環尋找 \(fail\) 結點實現,循環太屢次致使複雜度過高
上面提到的優化就是經過else語句的代碼修改了字典樹的結構。
而它將不存在的字典樹狀態鏈鏈接到失配指針的對應狀態。使得再次遍歷這裏的時候會繼續向下跳轉,起到一個經過繼續開鏈來壓縮路徑的效果,這樣就能節省不少時間。
這樣AC 自動機修改字典樹結構連出的邊就會使字典樹變爲字典圖

會不會影響原樹?在原字典樹中,每個結點表明一個字符串 ,是某個模式串的前綴。而在修改字典樹結構後,儘管增長了許多轉移關係,但結點(狀態)所表明的字符串是不變的。

多模式匹配

(這只是對於引例的query函數,具體題目的函數寫法可能不太相同)

int query(char *t){
		int u = 0, res = 0;
		for(int i = 1; t[i]; ++i){
			u = tr[u][t[i] - 'a'];
			for(int j = u; j && e[j] != -1; j = fail[j]){
				res += e[j], e[j] = -1;
			}
		}
		return res;
	}

這裏 \(u\) 做爲字典樹上當前匹配到的結點, \(res\) 即返回的答案。循環遍歷匹配串, \(u\) 在字典樹上跟蹤當前字符。利用 \(fail\) 指針找出全部匹配的模式串,累加到答案中。而後清零。對 \(cnt[j]\) 取反的操做用來判斷 \(cnt[j]\) 是否等於 \(-1\)。在上文中咱們分析過,字典樹的結構其實就是一個 \(trans\) 函數,而構建好這個函數後,在匹配字符串的過程當中,咱們會捨棄部分前綴達到最低限度的匹配。\(fail\) 指針則指向了更多的匹配狀態。

例題

P3808 【模板】AC自動機(簡單版)
P3796 【模板】AC自動機(增強版)
P5357 【模板】AC自動機(二次增強版)

相關文章
相關標籤/搜索