1. 程式人生 > 其它 >AC自動機[模板+圖講解]

AC自動機[模板+圖講解]

技術標籤:字串演算法我的模板類

目錄~


傳送門

下面的圖來源於這個博主^_^,直接搬過來方便一點


f a i l fail fail指標的匹配過程

先把 n n n個模式串建立字典樹

我們拿文字串 a b c d e abcde abcde進去匹配

一開始進入最左邊的分支 a b c d abcd abcd,成功匹配到一個!!!然後就沒然後了,然後開始從 b b b暴力匹配…

不是的,這裡我們也應該像 K M P KMP KMP那樣利用已經匹配的性質進行跳躍

我們定義 f a i l [ i ] fail[i] fail[i]為與節點 i i i失配後跳到的下一個節點

比如,匹配完 a b c d abcd abcd後,在 e e e位置失配…因為不存在這個節點了!!!

那麼我們可以直接跳到 b c d bcd bcd上的 d d d去,為什麼??

因為 b c d bcd bcd a b c d abcd abcd的一個最長字尾,已經匹配了 a b c d abcd abcd,也一定匹配 b c d bcd bcd

然後根據 b c d bcd bcd的節點 d d d f a i l fail fail跳到 c d cd cd上的 d d

d去,原因同上…

可以發現,每次通過 f a i l fail fail指標從節點 i i i跳到 j j j,那麼根到 j j j的路徑是根到 i i i路徑的一個最長字尾

既然是字尾,那麼一定會跳到深度比較小的節點去,假如沒有那個點,就跳回根節點

預處理 f a i l fail fail指標

所以根據這個性質,可以使用 b f s bfs bfs的方式預處理 f a i l fail fail指標

首先,根下面的那層字母的 f a i l fail fail指標都指向根節點,因為一個字母的字尾是自己,難道跳到自己去嗎哈哈

然後我們把根節點下面的所有節點入隊 b f s bfs bfs

每次令 u u

u出隊, u u u f a i l fail fail指標已知,處理 u u u兒子的 f a i l fail fail指標

下面這句話有點繞,需要理解一下

u u u兒子的 f a i l fail fail指標等於: u u u f a i l fail fail指標指向節點下的對應兒子


(下面三行都是對上面那句話的解釋)

我們這麼想,令 u u u f a i l fail fail指標指向 v v v

那麼根節點到 v v v的路徑是根節點到 u u u的路徑的一個字尾

現在根節點是到 u u u的兒子,顯然 v v v後面也需要加一個相同的兒子

這麼做非常正確,但是假如 v v v後面沒有對應的兒子呢??那麼我的 f a i l fail fail指標指向了哪裡呢??

別急,因為這是個 b f s bfs bfs,即使 v v v不存在對應的兒子,但那個位置的 f a i l fail fail指標是被我們處理過的

我們處理一下那個不存在節點的 f a i l fail fail指標即可,這裡看程式碼比較方便


void get_fail()
{
	queue<int>q;
	for(int i=0;i<26;i++)
		if( ac[0].vis[i] )	q.push( ac[0].vis[i] );//第一層的fail指標指向根節點0
	while( !q.empty() )
	{
		int u = q.front(); q.pop();
		for(int i=0;i<26;i++)
		{
			if( ac[u].vis[i] )//有子節點 
			{
				ac[ac[u].vis[i]].fail = ac[ac[u].fail].vis[i];
				q.push( ac[u].vis[i] );
			}
			else//沒有子節點
				ac[u].vis[i] = ac[ac[u].fail].vis[i];
			//沒有這個子節點,這個子節點指向當前節點fail指標的子節點.
			//假如fail指標指向的點也沒有這個兒子,沒關係,在之前的bfs已經處理過了
		}
	}
}

預處理完畢,開始 A C AC AC

艱難的 f a i l fail fail指標構建完畢,接下來就丟進去暴力匹配就好了

int AC_query( char a[] )
{
	int n = strlen( a+1 );
	int now = 0, ans = 0;
	for(int i=1;i<=n;i++)
	{
		now = ac[now].vis[a[i]-'a'];//從[1,i]開始利用fail指標,跳躍字尾
		for(int t=now;t&&ac[t].end!=-1;t=ac[t].fail)
			ans += ac[t].end, ac[t].end = -1;//經過的節點計算了答案,標記起來下次不走
	}
	return ans;
}

下面是完整程式碼

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+10;
struct tree
{
	int fail,vis[27],end;
}ac[maxn]; int id,n;
char a[maxn];
void insert(char a[] )
{
	int n = strlen( a+1 ), now = 0;
	for(int i=1;i<=n;i++)
	{
		if( !ac[now].vis[a[i]-'a'] )	ac[now].vis[a[i]-'a'] = ++id;
		now = ac[now].vis[a[i]-'a'];
	}
	ac[now].end++;
}
void get_fail()
{
	queue<int>q;
	for(int i=0;i<26;i++)
		if( ac[0].vis[i] )	q.push( ac[0].vis[i] );
	while( !q.empty() )
	{
		int u = q.front(); q.pop();
		for(int i=0;i<26;i++)
		{
			if( ac[u].vis[i] )//有子節點 
			{
				ac[ac[u].vis[i]].fail = ac[ac[u].fail].vis[i];
				q.push( ac[u].vis[i] );
			}
			else
				ac[u].vis[i] = ac[ac[u].fail].vis[i];
			//沒有這個子節點,這個子節點指向當前節點fail指標的子節點 
		}
	}
}
int AC_query( char a[] )
{
	int n = strlen( a+1 );
	int now = 0, ans = 0;
	for(int i=1;i<=n;i++)
	{
		now = ac[now].vis[a[i]-'a'];
		for(int t=now;t&&ac[t].end!=-1;t=ac[t].fail)
			ans += ac[t].end, ac[t].end = -1;
	}
	return ans;
}
int main()
{
	cin >> n;
	for(int i=1;i<=n;i++)
		scanf("%s",a+1),insert( a );
	get_fail();
	scanf("%s",a+1);
	printf("%d",AC_query(a) );
}