【算法】AC自動機(ACAM)


這篇文章不僅寫了普通的原算法,還附了好幾道好題和題解,如果大佬您已經學了 ACAM,不妨做做文后的題目,如果這些題您也都做過了,那就點個贊支持一下吧!

upd 8.29:把之前咕掉的三道題補上了。

前置知識

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;
}

一些題目

比較板子的題就沒放了啊。

ACAM上的dp

ACAM 上的 dp 比較套路,大多數狀態都是設為 \(f[\)串長\(][\)目前節點\(]\)

1.USACO12JAN-Video Game G

本題就是直接按上面的狀態枚舉出邊,進行轉移,比較板。

code:

#include<cstdio>
#include<queue>
#define re register
using std::queue;
inline int max(int x,int y){return x>y?x:y;}

const int L=18,N=365,M=1005;
int n,m,tot,ans,end[N],fail[N],tr[N][3],f[M][N];

inline void ins(){
	char c[L];int x=0,i=0,p;
	scanf("%s",c+1);
	while(c[++i]) p=c[i]-'A',x=tr[x][p]?tr[x][p]:(tr[x][p]=++tot);
	end[x]++;
}
inline void buildfail(){
	queue<int> q;int x;
	for(re int k=0;k<3;k++) if(tr[0][k]) q.push(tr[0][k]);
	while(!q.empty()){
		x=q.front(),q.pop();
		for(re int k=0;k<3;k++)
			if(tr[x][k]){
				q.push(tr[x][k]);
				fail[tr[x][k]]=tr[fail[x]][k];
				end[tr[x][k]]+=end[fail[tr[x][k]]];
			}
			else tr[x][k]=tr[fail[x]][k];
	}
}
inline void dp(){
	for(re int i=0;i<=m;i++)
		for(re int j=0;j<=tot;j++)
			f[i][j]=-0x3f3f3f3f;
	f[0][0]=0;
	for(re int i=0;i<=m;i++)
		for(re int j=0;j<=tot;j++)
			for(re int k=0;k<3;k++)
				f[i+1][tr[j][k]]=max(f[i+1][tr[j][k]],f[i][j]+end[tr[j][k]]);
}

int main(){
	scanf("%d %d",&n,&m);
	for(re int i=1;i<=n;i++) ins();
	buildfail(),dp();
	for(re int i=0;i<=tot;i++) ans=max(f[m][i],ans);
	printf("%d",ans);
	return 0;
}

2.JSOI2007-文本生成器

正着算會重,本題目要正難則反。

先算出所有可生成文本數量(快速冪)。

再減去一個點也匹配不到的文本(就是在 Trie 上沒經過任何一個串的末尾的文本)數量(dp 求)。

code:

#include<cstdio>
#include<queue>
#define re register
using std::queue;

const int L=105,N=6050,M=105,mod=10007;
int n,m,tot,fail[N],tr[N][26],f[M][N],ans=1;
bool end[N];

inline void ins(){
	char c[L];int i=0,x=0,p;
	scanf("%s",c+1);
	while(c[++i]) p=c[i]-'A',x=(tr[x][p]?tr[x][p]:(tr[x][p]=++tot));
	end[x]=1;
}
inline void buildf(){
	queue<int> q;int x;
	for(re int k=0;k<26;k++) if(tr[0][k]) q.push(tr[0][k]);
	while(!q.empty()){
		x=q.front(),q.pop();
		for(re int k=0;k<26;k++){
			if(tr[x][k]){
				fail[tr[x][k]]=tr[fail[x]][k];
				end[tr[x][k]]|=end[fail[tr[x][k]]];
				q.push(tr[x][k]);
			}
			else tr[x][k]=tr[fail[x]][k];
		}
	}
}
inline void qpow(int d,int z){for(;z;z>>=1,d=d*d%mod) ans=(z&1)?ans*d%mod:ans;}
inline void dp(){
	f[0][0]=1;
	for(re int i=0;i<m;i++)
		for(re int j=0;j<=tot;j++)
			for(re int k=0;k<26;k++)
				if(!end[tr[j][k]])
					f[i+1][tr[j][k]]=(f[i+1][tr[j][k]]+f[i][j])%mod;
	for(re int j=0;j<=tot;j++) ans=(ans-f[m][j])%mod;
}

int main(){
	scanf("%d %d",&n,&m);
	for(re int i=1;i<=n;i++) ins();
	buildf(),qpow(26,m),dp();
	printf("%d\n",ans>=0?ans:(ans+mod));
	return 0;
}

fail樹上的問題

1.NOI2011-阿狸的打字機

首先根據輸入建出 Trie 樹。

利用一個字符串數據結構的常用性質:子串是前綴的后綴。

所以,我們可以對於每個 \(y\) 的前綴所對應的節點看它 fail 樹上的祖先是否包括 \(x\) 所對應的節點,如果有就說明 \(x\) 在此位置為 \(y\) 的子串,更新一下答案。

換種說法,就是求每個 \(y\) 的前綴所對應的節點里有多少個節點在 \(x\) 所對應的節點在 fail 樹上的子樹上。

所以可以處理出 fail 樹的 dfs 序,把所有詢問離線掉,在 Trie 上 dfs 將點加入樹狀數組,用樹狀數組處理以目前節點為 \(y\) 所對應的節點的詢問。

注意本題要在 Trie 上 dfs,所以不能建字典圖。

code:


#include<cstdio>
#include<queue>
#include<stack>
#define re register
#define fuc(k) for(re int k=0;k<26;k++)
using std::queue;
using std::stack;
inline int win(){
	int x=0;char c=getchar();
	while(c>'9'||c<'0') c=getchar();
	while(c>='0'&&c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x;
}

const int N=100050;
char s[N];
int tot,t,ch[N][26],fail[N],endof[N];
int t2,cnt,h2[N],v2[N],ne2[N],xl[N],xr[N];
int t1,h1[N],v1[N],ne1[N],id[N],ans[N];

struct BIT{
	int f[N];
	inline void add(int x){for(;x<=cnt;x+=(x&-x)) f[x]++;}
	inline void del(int x){for(;x<=cnt;x+=(x&-x)) f[x]--;}
	inline int ask(int l,int r){
		int res=0;l--;
		for(;r;r^=(r&-r)) res+=f[r];
		for(;l;l^=(l&-l)) res-=f[l];
		return res;
	}
}bit;
inline void rem(int y,int x,int p){v1[++t1]=x,ne1[t1]=h1[y],h1[y]=t1,id[t1]=p;}
inline void add(int x,int y){v2[++t2]=y,ne2[t2]=h2[x],h2[x]=t2;}
inline void buildtrie(){
	stack<int> st;int x=0,i=0,p;scanf("%s",s+1),st.push(0);
	while(s[++i]){
		if(s[i]=='B') st.pop(),x=st.top();
		else if(s[i]=='P') endof[++t]=tot;
		else p=s[i]-'a',st.push(x=ch[x][p]=ch[x][p]?ch[x][p]:++tot);
	}
}
inline void buildfail(){
	queue<int> q;int x,j;
	fuc(k) if(ch[0][k]) q.push(ch[0][k]);
	while(!q.empty()){
		x=q.front(),q.pop(),add(fail[x],x);
		fuc(k) if(ch[x][k]){
			j=fail[x];while(!ch[j][k]&&j) j=fail[j];
			fail[ch[x][k]]=ch[j][k],q.push(ch[x][k]);
		}
	}
}
void dfs1(int x){
	xl[x]=++cnt;
	for(re int i=h2[x];i;i=ne2[i]) dfs1(v2[i]);
	xr[x]=cnt;
}
void dfs2(int x){
	bit.add(xl[x]);
	for(re int i=h1[x];i;i=ne1[i]) ans[id[i]]=bit.ask(xl[v1[i]],xr[v1[i]]);
	fuc(k) if(ch[x][k]) dfs2(ch[x][k]);
	bit.del(xl[x]);
}

int main(){
	buildtrie(),buildfail();int m=win();
	for(re int i=1,x;i<=m;i++) x=endof[win()],rem(endof[win()],x,i);
	dfs1(0),dfs2(0);
	for(re int i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
}



其他一些好題

1.POI2000-病毒

考慮用輸入的 01 串構造 ACAM 並標記其結尾,本題即是問是否能在 ACAM 上走出一條無限長的路徑使它不經過任何一個帶有結尾標記的節點。

顯然,要無限長的這種路徑,必然意味着從根出發不經過帶有結尾標記的節點能走到一個環,滿足這個環上沒有一個帶有結尾標記的節點。

所以建出來后 dfs 判環即可,注意本題不可拓撲判環,因為刪掉帶結尾標記的點后圖不一定連通,難以判斷是否從根出發能走出環。

code:

#include<cstdio>
#include<queue>
#define re register
using std::queue;

const int N=3e4+5;
int t,tr[N][2],fail[N],lin[N];
bool end[N],vis[N];

inline void add(){
	int x=0,p;char c=getchar();
	while(c!='0'&&c!='1') c=getchar();
	while(c=='0'||c=='1'){
		p=c=='1';
		x=(tr[x][p]?tr[x][p]:tr[x][p]=++t);
		c=getchar();
	}
	end[x]=1;
}
inline void buildfail(){
	int x;queue<int> q;
	q.push(tr[0][0]),q.push(tr[0][1]);
	while(!q.empty()){
		x=q.front(),q.pop();
		for(re int k=0;k<=1;k++)
			if(tr[x][k]) q.push(tr[x][k]),fail[tr[x][k]]=tr[fail[x]][k],end[tr[x][k]]|=end[fail[tr[x][k]]];
			else tr[x][k]=tr[fail[x]][k];
	}
}
inline bool dfs(int x){
	if(vis[x]) return true;
	vis[x]=1;
	int k=(((!end[tr[x][0]])&&dfs(tr[x][0]))||((!end[tr[x][1]])&&dfs(tr[x][1])));
	return vis[x]=0,k;
}
/*
inline void debug(){
	for(re int i=0;i<=t;i++){
			printf("%d: %d %d\n",i,tr[i][0],tr[i][1]);
		}
}
*/
int main(){
	int n;
	scanf("%d",&n);
	while(n--) add();
	if(!(tr[0][0]&&tr[0][1])) return puts("TAK"),0;
//	debug();
	buildfail();
//	printf("%d %d\n",cnt,tot);
	puts(dfs(0)?"TAK":"NIE");
	return 0;
}

2.CF163E e-Government

本題我投了題解,就不再寫一遍了。

完 ! 結 ! 撒 ! 花 !


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM