1. 程式人生 > 實用技巧 >[題解][筆記]AC自動機

[題解][筆記]AC自動機

[題解][筆記]AC自動機

演算法概述:

AC自動機的用途主要是在一個文字串中查詢多個短字串,它的主要構成就是一顆\(trie\)樹再加上一些\(kmp\)的思想.

模擬演算法過程:

​ 首先給定\(4\)個模式串:\(ash\),\(shex\),\(bcd\),\(sha\).並建立一顆\(trie\)樹.

接著,我們之前說過\(ac\)自動機就是\(trie\)樹加\(kmp\)的思想,那麼\(kmp\)\(border\)用來提升效率,\(ac\)自動機也有相類似的結構:\(fail\)指標.它的用處就是如果當前點失配了就直接把指標移動到\(fail\)指標所指的節點,就不需要回溯了.(\(fail\)

指標指向的要求:當前模式串字尾和\(fail\)指標指向的模式串的字首是相同的)例如:當前找的是\(abce\),但我們發現要匹配的第四位發現並不是要找\(e\),y於是跳\(fail\)指標,看\(bcd\)符不符合要求.(注意:上文括號中的當前模式串是指從根節點到這個節點上所有字元組成的字串)

​ 構造\(fail\)指標通常通過廣搜的辦法實現.對於根節點的兒子節點,它們的\(fail\)指標直接指向根節點,因為它們沒有後綴或字首.對於其他節點,如果有一樣的節點就直接相連,如果,沒有一樣的節點就連到根節點.

看程式碼吧,裡面有註釋

#include <bits/stdc++.h> 
using namespace std;
char str[1000010];
struct node{
	int fail;//失配指標
	int cnt;//單詞出現的次數
	int next[62];//兒子節點
}trie[1000010];
int k = 0,ans = 0;
queue< int > q;
void build_trie(int id,char s[]){
	int len = strlen(s);//字串的長度,也是這個字串在trie樹中的深度(可以看上圖理解下)
	int j = 0;
	for(int i = 0;i < len;i++){//遍歷輸入的字串
	    j = s[i] - 'a';
	    if(trie[id].next[j]==0){//如果當前節點的兒子中還沒有這個字元就新建節點
	        trie[id].next[j]= ++k;//節點個數+1
	    }
	    id = trie[id].next[j];//因為是建trie樹,所以建完的字串是一個從上到下的鏈,所以建完當前節點就建它的兒子節點
	}
	trie[id].cnt++;//單詞數量+1
}
void build_fail(int id){
	while(!q.empty())q.pop();
	for(int i = 0;i < 26;i++){//遍歷根節點的所有兒子節點
	    int j = trie[id].next[i];
	    if(j != 0){//如果存在這個兒子就入隊
	        q.push(j);
	        trie[j].fail = id;//根節點的兒子的fail指標就是根節點
	    }
	}
	while(!q.empty()){//開始廣搜
	    int now = q.front();
		q.pop();
	    for(int i = 0;i < 26;i++){//遍歷當前節點的兒子
	        int j = trie[now].next[i];
	        if(j == 0){//如果沒有這個兒子
	            trie[now].next[i] = trie[trie[now].fail].next[i];//它的兒子是它父親fail指標指向的節點的對應的兒子
                //因為trie數是1~26代表a~z,所以next數組裡的第i個字母一定對應的是字母表中的第i個字元
                //所以如果當前節點沒有"h"這個兒子,那麼在它指向的節點一定也是"h"
	            continue;
	        }
	        trie[j].fail = trie[trie[now].fail].next[i];//當前節點的兒子的fail指標指向它父親fail指標指向的節點的對應的兒子
	        q.push(j);
	    }
	}
}
void sovle(int id,char s[]){
	int len = strlen(s),j = 0;
	for(int i=0;i<len;i++){
	    int j = trie[id].next[s[i] - 'a'];
	    while(j && trie[j].cnt != -1){//如果存在這個節點並且這個節點有構成單詞
	        ans += trie[j].cnt;//答案累計合法單詞的數量
	        trie[j].cnt = -1;//不能重複累加
	        j = trie[j].fail;//直接指向它的失配指標
	    }
	    id = trie[id].next[s[i] - 'a'];//往下找
	}
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i = 1;i <= n;i++){
	    scanf("%s",str);
	    build_trie(0,str);
	}
	build_fail(0); 
	scanf("%s",str);
	sovle(0,str); 
	printf("%d\n",ans); 
	return 0;
}