1. 程式人生 > 其它 >學習筆記——AC自動機

學習筆記——AC自動機

前言

$AC$自動機的題相對而言較為套路,但重在理解其思維,瞭解每一個數組的含義及一些拓展用法,就可以了。(模板一定要打對,不然真就成$WA$自動機了)

$AC$自動機的一些概念

我們都知道,$KMP$用於解決單模式串與多文字串的匹配問題,$Trie$樹用於實現字串快速檢索,$AC$自動機就是兩者的結合,用於解決多模式串匹配問題。

首先,我們可以仿照$Trie$樹,將每一個模式串都插入到$Trie$樹上,再在$Trie$樹上建立失配陣列,然後按照正常的$Trie$樹檢索和$KMP$匹配失配指標的方式和文字串進行匹配即可。

1.建立$AC$自動機

char str[N];
int trie[N][26],end[N],fail[N],cnt=0;
//trie就是trie樹的陣列,end就是記錄結尾節點的陣列,fail就是失配陣列
il void insert() {
	scanf("%s",str+1);
	int id=0,len=strlen(str+1);
	for(re int i=1;i<=len;++i) {
		int to=str[i]-'a';
		if(!trie[id][to]) trie[id][to]=++cnt;
		id=trie[id][to];
	}	
	++end[id];
}	
//同Trie的建樹操作

2.構建失配陣列

il void get_fail() {
	queue <int> q;//存Trie樹上的節點
	for(re int i=0;i<26;++i) {
		if(trie[0][i])	//如果有這個點,就加入佇列
			q.push(trie[0][i]);		
         	//如果trie樹的起點不是0,是st,還要加一句fail[trie[0][i]]=st
	}
	while(!q.empty()) {
		int u=q.front();q.pop();
		for(re int i=0;i<26;++i) {
			if(trie[u][i]) {
				fail[trie[u][i]]=trie[fail[u]][i];
				//從u節點更新其兒子節點的失配指標
				//其兒子節點的失配指標指向u節點的失配指標指向的節點的邊權相同的節點 
				//此處不用一直跳fail指標,因為u節點的fail指標更新先於其兒子的fail指標 
				q.push(trie[u][i]);
				//有這個點,就加入佇列
			}
			else trie[u][i]=trie[fail[u]][i];
			//沒有這個點,就把這個點變為u節點失配指標指向的邊權相同的點 
		}
	}
}

這一步是$AC$自動機的精髓,光看程式碼可能還是比較難理解,下面給張圖解釋一下:

這是我們插入$Trie$樹中的文字串

這裡的虛線就是該節點的失配指標,類似$KMP$,當匹配失配時,我們可以通過跳$fail$來重新匹配,將字串匹配複雜度降低。(還是不會用畫圖軟體,將就看吧)

3.查詢操作

以下程式碼為查詢模式串在文字串中的出現次數。

il int ask() {
	scanf("%s",str+1);//輸入文字串 
	int id=0,len=strlen(str+1),res=0;
	for(re int i=1;i<=len;++i) {
		int to=str[i]-'a';
		id=trie[id][to];
		for(re int j=id;j&&end[j]!=-1;j=fail[j]) 
		//跳fail指標,看是不是一個模式串的結束節點 
			res+=end[j],end[j]=-1;
			//是就統計答案,並清空防止以後再被訪問到 
	}
	return res;
}

下面就是$AC$自動機的完整程式碼了:

#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <queue>
#define il inline
#define ll long long
#define int long long
#define re register
#define gc getchar
using namespace std;
//------------------------初始程式-------------------------- 
il int read(){
	re int x=0;re bool f=0;re char ch=gc();
	while(!isdigit(ch)){f|=ch=='-';ch=gc();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=gc();}
	return f?-x:x;
}

il int max(int a,int b){
	return a>b?a:b;
}

il int min(int a,int b){
	return a<b?a:b;
}


//------------------------初始程式-------------------------- 

const int N=1e6+10;
int n;
char str[N];
int trie[N][26],end[N],fail[N],cnt=0;
//trie就是trie樹的陣列,end就是記錄結尾節點的陣列,fail就是失配陣列
il void insert() {
	scanf("%s",str+1);
	int id=0,len=strlen(str+1);
	for(re int i=1;i<=len;++i) {
		int to=str[i]-'a';
		if(!trie[id][to]) trie[id][to]=++cnt;
		id=trie[id][to];
	}	
	++end[id];
}	

il void get_fail() {
	queue <int> q;//存Trie樹上的節點
	for(re int i=0;i<26;++i) {
		if(trie[0][i])	//如果有這個點,就加入佇列
			q.push(trie[0][i]);		
         	//如果trie樹的起點不是0,是st,還要加一句fail[trie[0][i]]=st
	}
	while(!q.empty()) {
		int u=q.front();q.pop();
		for(re int i=0;i<26;++i) {
			if(trie[u][i]) {
				fail[trie[u][i]]=trie[fail[u]][i];
				//從u節點更新其兒子節點的失配指標
				//其兒子節點的失配指標指向u節點的失配指標指向的節點的邊權相同的節點 
				//此處不用一直跳fail指標,因為u節點的fail指標更新先於其兒子的fail指標 
				q.push(trie[u][i]);
				//有這個點,就加入佇列
			}
			else trie[u][i]=trie[fail[u]][i];
			//沒有這個點,就把這個點變為u節點失配指標指向的邊權相同的點 
		}
	}
}

il int ask() {
	scanf("%s",str+1);//輸入文字串 
	int id=0,len=strlen(str+1),res=0;
	for(re int i=1;i<=len;++i) {
		int to=str[i]-'a';
		id=trie[id][to];
		for(re int j=id;j&&end[j]!=-1;j=fail[j]) 
		//跳fail指標,看是不是一個模式串的結束節點 
			res+=end[j],end[j]=-1;
			//是就統計答案,並清空防止以後再被訪問到 
	}
	return res;
}

signed main()
{
	n=read();
	for(re int i=1;i<=n;++i) insert();
	get_fail();
	printf("%lld\n",ask());
	return 0;
}

接下來,我們就可以開始我們的刷題之路了:

P3808 【模板】AC自動機(簡單版)(學習程式碼的題解)(有圖講解的題解)(我的程式碼)

P3796 【模板】AC自動機(加強版)(題解)(我的程式碼)

修改查詢函式,改為記錄模式串出現次數,最後$sort$一遍把出現次數相同的最長的字串輸出即可。

P5357 【模板】AC自動機(二次加強版)(題解)(我的程式碼)

本題要對$fail$陣列進行理解,將其建成一顆$fail$樹,統計$Trie$的終止節點在$fail$樹的子樹上的總匹配次數。

理解:因為$fail$樹的一個節點的子樹的所有節點都可以通過失配指標跳到當前節點,所以他們都有相同的字首(從$trie$樹的根節點到該節點),所以模式串出現次數就是$fail$樹上該節點子樹的文字串出現次數。

P3121 [USACO15FEB]Censoring G(題解)(我的程式碼)

本題要運用棧的思想,分別記錄$AC$自動機掃到的節點和合法的字元,如果$AC$自動機匹配到了就將兩個棧同時彈出匹配到的長度,最後輸出合法字元棧內的字元即可。

P3966 [TJOI2013]單詞(題解)(我的程式碼)

本題是$AC$自動機二次加強版的板子,只要改一下模式串就可以得到結果(雙倍經驗)

P3041 [USACO12JAN]Video Game G(題解)(我的程式碼)

本題是$AC$自動機$+ DP$,我們用$dp[i][j]$表示長度為$i$的字串在$Trie$樹上編號為$j$的節點結束的最大得分,再列舉下一個是$ABC$三選一的情況時的匹配次數,找哪個貢獻最大,最後取長度為$k$時的最大值即可。

P4052 [JSOI2007]文字生成器(題解)(我的程式碼)

本題同樣是$AC$自動機$+ DP$,我們用$dp[i][j]$表示長度為$i$且結束節點為$j$的不合法字串數(沒有走到一個字串的結束節點),再運用容斥原理用總數減去長度為$m$的所有不合法字串總數即可。

P2444 [POI2000]病毒(題解)(我的程式碼)

本題要找無限長的可行串,我們可以轉化一個思路:將$Trie$樹連向兒子節點的邊看做單向邊,再將該節點連向其失配節點的邊看做單向邊,再在$Trie$樹上找環(該環不能經過任意病毒串的結束節點),找的到就有無限長的可行串。(因為可以一直繞著那個環走)

P2414 [NOI2011] 阿狸的打字機(我的程式碼)

CF1207G Indie Album(我的程式碼)

因為以上是兩道一樣的題,所以給一個提供思路的題解,只要處理一下不同的輸入方式即可。

思路:因為要判斷第$x$個字串在第$y$個字串中出現的次數,所以我們可以構造一顆$fail$樹,判斷以$x$的結束節點為根的子樹中,有多少個節點屬於$y$字串(因為如果$y$字串的$fail$指標指向$x$字串,那麼$x$字串一定在$y$字串中出現過(可結合上面的$fail$樹圖進行理解)),對於這個問題,我們可以運用樹鏈剖分的思想,將$fail$樹的每一個節點標記一個$dfs$序,並統計每一個節點的子樹大小,那麼$u$節點子樹對應的區間就是$[dfn[u],dfn[u]+siz[u]-1]$,用樹狀陣列維護即可。

刷題題單

菜雞L_C_A的基礎字串(KMP&ACAM)

參考資料

《資訊學奧賽一本通提高篇》