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

【學習筆記】AC自動機

  • 還沒學完,我先寫著

前置知識

kmp模式匹配、Trie

沒了

介紹:這玩意是幹什麼的

想必你第一次看到AC自動機這個名字,心潮湧動

其實這和做題AC啥關係沒有,這個AC是Aho-Corasick

你一定知道kmp是在一個文字串中匹配一個模式串

AC自動機(ACam/ACAM:Aho-Corasick automaton)可以讓你在一個文字串中匹配一堆模式串,但文字串只要掃一遍,很是NB

通俗的解釋:看了也沒有幫助


正經板子

  • 搞一個演算法,搞一個數據結構,都要從裸的搞起,學會了搞裸的,才能搞好別的。

裸T:

1st
2nd
3rd

此處以3rd為例講解(這就是ACAM最經典的運用):

建立

AC自動機的實現結合Trie的結構KMP的失配指標思想,構造過程可分為建立Trie與構造失配指標兩個部分:

如何建立Trie

和普通的Trie構建一模一樣,把所有模式串插入一個Trie(此處所用Trie陣列下文稱 \(tr\) )即可,此處不再贅述

為便於下文敘述,對於節點 \(x\) ,將Trie的根到\(x\)的路徑所表示的字串記為 \(S_{x}\)

構造失配指標
定義

對於節點 \(x\) ,其失配指標(下文稱 \(fail\) 指標)指向Trie中不為 \(x\) 的一個節點 \(y\) ,滿足 \(S_{y}\)\(S_{x}\) 的字尾,且 \(S_{y}\) 最長

思想與步驟

建議使用OI-wiki的這個例子幫助理解。

大致參考KMP的思想,在對Trie進行搜尋(BFS)的過程中,設當前節點為\(x\)

1.找到 \(x\) 的父親節點 \(p\)\(x\) 的邊上字元 \(c\) (即 \(tr_{p,c}=x\) ),令 \(y=fail_{p}\)

2.若 \(tr_{p,c}\) 存在,則顯然 \(S_{tr_{p,c}}\)\(S_{x}\) 字尾,又因每跳一次 \(fail\)\(p\) 的深度就會減小,故此時的 \(p\) 即為 \(fail_{x}\) ;若不存在,則 \(p \leftarrow fail_{p}\) ,並重復此步驟直至 \(p\)

為根

字典圖

一個優化,對每個 \(tr_{x,c}\) ,若不為空則不變,若為空則 \(tr_{x,c} \leftarrow tr_{fail_{x},c}\)

這樣,對每個 \(tr_{x,c}\) 都有 \(fail_{tr_{x,c}}=tr_{fail_{x},c}\) ,節省了多餘空間,求 \(fail\) 陣列和匹配文字串時常數更小了,更容易寫了,大家都說好

code
inline void buildfail(){
	int x;queue q;
	for(re int i=0;i<26;++i)
		if(tr[0][i]) q.push(tr[0][i]);
		while(q.unempty()){
			x=q.front(),q.pop();
			for(re int i=0;i<26;++i) 
				if(tr[x][i]) q.push(tr[x][i]),fail[tr[x][i]]=tr[fail[x]][i];
				else tr[x][i]=tr[fail[x]][i];
		}
}

匹配

步驟

很明顯,ACAM建立以後,若匹配字串 \(T\) ,就一直對節點 \(x \leftarrow tr_{x,T_{i}}\) ,到達的 \(x\) 點就表示 \(S_{x}\)\(T\) 的一個子串,記錄 \(x\) 被到達次數即可

但是還有一個東西

對於節點 \(x\) ,其失配指標指向Trie中不為 \(x\) 的一個節點 \(y\) ,滿足 \(S_{y}\)\(S_{x}\) 的字尾,且 \(S_{y}\) 最長

也就是說,不止$ S_{x} $ 是 \(T\) 的一個子串, $ S_{fail_{x}} , S_{fail_{fail_{x}}} ......$ 都是 \(T\) 的子串

那怎麼辦?如果在每個節點迴圈跳 \(fail\) ,肯定會TLE ( 也可能卡過去,我沒試 )

但是,每個點的 \(fail\) 指標只有一個,並且根節點沒有 ( 或者說,在程式中為自己 ) !

也就是說, $ fail $ 為一顆樹,所以,匹配完以後,在fail樹上dfs或拓撲排序,就可以把 \(x\) 節點的資訊傳到它的 \(fail\) 上了!

最後對Trie圖上是模式串結尾的節點統計答案就好了

程式碼
inline void query(){
	int i=0,x=0;scanf("%t",t+1);
	while(t[++i]) x=tr[x][t[i]-'a'],++siz[x];
	topo();
}
inline void topo(){
	int x;queue q;
	for(re int i=1;i<=cnt;++i) ++in[fail[i]];
	for(re int i=1;i<=cnt;++i) if(!in[i]) q.push(i);
	while(q.unempty()){
		x=q.front();q.pop();
		if(end[x]) ans[end[x]]+=siz[x];
		siz[fail[x]]+=siz[x];
		if(!(--in[fail[x]])) q.push(fail[x]);
	}
}

一個小細節

此題有重複模式串,開鄰接表存一下就是

code

#include<cstdio>
#include<cstring>
#define re register
const int N=2e6+5,M=2e5+5;
int ans[M],nxt[M];
char s[N];
struct queue{
	int l=1,r=0,a[M];
	inline bool unempty(){return l<=r;}
	inline int front(){return a[l];}
	inline void pop(){++l;}
	inline void push(int x){a[++r]=x;}
};
struct ACam{
	int cnt,tr[M][26],end[M],fail[M],siz[M],in[M];
	inline void ins(int k){
		int i=0,x=0;scanf("%s",s+1);
		while(s[++i]) x=tr[x][s[i]-'a']=(tr[x][s[i]-'a']?tr[x][s[i]-'a']:++cnt);
		nxt[k]=end[x],end[x]=k;
	}
	inline void buildfail(){
		int x;queue q;
		for(re int i=0;i<26;++i)
			if(tr[0][i]) q.push(tr[0][i]);
		while(q.unempty()){
			x=q.front(),q.pop();
			for(re int i=0;i<26;++i) 
				if(tr[x][i]) q.push(tr[x][i]),fail[tr[x][i]]=tr[fail[x]][i];
				else tr[x][i]=tr[fail[x]][i];
		}
	}
	inline void topo(){
		int x;queue q;
		for(re int i=1;i<=cnt;++i) ++in[fail[i]];
		for(re int i=1;i<=cnt;++i) if(!in[i]) q.push(i);
		while(q.unempty()){
			x=q.front();q.pop();
			if(end[x]) ans[end[x]]+=siz[x];
			siz[fail[x]]+=siz[x];
			if(!(--in[fail[x]])) q.push(fail[x]);
		}
	}
	inline void query(){
		int i=0,x=0;scanf("%s",s+1);
		while(s[++i]) x=tr[x][s[i]-'a'],++siz[x];
		topo();
	}
}ac;

int main(){
	int n;scanf("%d",&n);
	for(re int i=1;i<=n;++i) ac.ins(i);
	ac.buildfail();ac.query();
	for(re int i=n;i;--i) ans[nxt[i]]=ans[i];
	for(re int i=1;i<=n;++i) printf("%d\n",ans[i]);
	return 0;
}

一些題目

先鴿,Garrison叫我去打球