1. 程式人生 > >AC自動機的優化及經典例題

AC自動機的優化及經典例題

AC自動機是一種用於解決多模式串匹配問題的工具。

模板題:給定n個模式串和1個母串(由小寫字母組成),將母串中包含模式串的部分變為"*"號。

判斷一個串是不是另一個串的子串,我們首先會想到KMP演算法,但KMP演算法需要逐個處理每一個模式串,n太大時顯然會超時。這時,AC自動機便派上了用場,它的核心也是熟悉的next陣列,我們可以把它看做trie樹上的KMP。首先,我們把所有模式串加入一棵trie樹中(注意,我們要把trie樹的根結點設為1,原因下面會說),接著,我們通過bfs求出trie樹上每一個結點的next值(next的含義和KMP中沒有實質的差別),程式碼如下。

q[++tail]=1;
for(int i=1;i<=26;i++) son[0][i]=1;
while(tail>head)
{
	tmp=q[++head];
	for(int i=1;i<=26;i++)
		if(son[tmp][i])
		{
			q[++tail]=son[tmp][i],now=next[tmp];
			while(!son[now][i]) now=next[now];
			next[son[tmp][i]]=son[now][i];
		}
}

以上程式碼的前兩行看起來有些奇怪,第一行把trie樹的根結點1加入佇列,第二行又設定了一個0號結點,並把它的所有子結點都設為1。其實,這樣做是為了避免匹配中的一些特殊情況:假設遇到一個trie樹上無法找到的字元i,對於任何一個now,都滿足son[now][i]=0,當now=0時,next[now]=0,程式就會無限迴圈。為了避免這樣的問題,我們把trie樹的根結點設為1,當now=0時,無論i取何值,都滿足son[0][i]=1,從而退出迴圈。建出next陣列後,我們開始匹配母串,即讓母串在這棵trie樹上順著next陣列跑,記一個tmp表示當前到達的結點,但還有一個細節要注意:統計答案時,我們不應只統計tmp所在結點的答案,還應統計每一個tmp所在結點順著next陣列能到達的結點(只要對於每一個tmp,再開一個now=tmp,順著next陣列跑一跑就行了),否則會出現如下情況:

hack資料:

輸入:2

gui

u

guigu

輸出:***gu(漏掉了u

為什麼會出現這樣的情況呢?有些模式串可能是其他模式串的子串,所以會被遺漏...程式碼如下。

gets(s+1),m=strlen(s+1),tmp=1;
for(int i=1;i<=m;i++)
{
	ch=s[i]-96;
	while(!son[tmp][ch]) tmp=next[tmp];
	tmp=son[tmp][ch],now=tmp;
	while(now)
	{
		if(vis[now])
			for(r int j=1;j<=len[now];j++) s[i-j+1]='*';
    //len[now]表示以now結點結尾的模式串的長度,匹配到一個模式串,就要把它在母串中的部分全部變成"*"號
		now=next[now];
	}
}

例題二、BZOJ3940

我做這題時也按照上面記now=tmp的方法,結果超時了好幾發,後來仔細看看題目,已經保證了一個模式串不可能為其他模式串的子串,因此根本不需要now這個變數,只需要直接判斷vis[tmp]即可。這道題和上一題有一個區別:刪掉一個模式串後,兩邊剩餘的母串可能會拼出新的模式串,那應該如何操作呢?我一開始想到開一個to陣列表示母串中每一個字元的下一個字元,核心程式碼如下。

if(vis[tmp]) to[i-dep[tmp]]=i+1,i=1;

結果又WATLE...WA的原因有二。一:每次刪除一個模式串,就從頭再開始匹配,但tmp並沒有變為1。二:如果刪除一個模式串後,兩邊的母串會拼成一個新的模式串,to陣列指向的位置就會出錯,因為dep[tmp]沒有相應變化。TLE的原因也顯而易見:每次刪除一個模式串,就從頭再開始匹配,造成了極大的時間浪費,其實我們只要從被刪模式串的前一位繼續匹配即可,但是注意,不能直接把tmp變為1,而要開一個loc陣列記錄匹配到母串的每一位時tmp所在的位置。解決了TLE的問題,如何解決WA的問題呢?其實只要開一個棧就可以了...棧中的元素不能是字元,而是i,否則繼續WA...匹配程式碼如下。

tmp=1;
for(int i=1;i<=m;i++)
{
	ans[++top]=i,ch=s[i]-96;
	while(!son[tmp][ch]) tmp=next[tmp];
	loc[i]=tmp=son[tmp][ch];
	if(dep[tmp]) top-=dep[tmp],tmp=loc[ans[top]];
}

AC自動機的重要優化:構建trie圖。

AC自動機的複雜度是什麼?這是個值得思考的問題,for迴圈中的while看起來十分礙事,事實上,它也的確能被某些特殊資料卡到TLE,有沒有什麼辦法去掉中間的while迴圈呢?構建trie圖即可。什麼是trie圖?程式碼如下。

q[++tail]=1;
for(int i=1;i<=26;i++) son[0][i]=1;
while(tail>head)
{
	tmp=q[++head];
	for(r int i=1;i<=26;i++)
		if(!son[tmp][i]) son[tmp][i]=son[next[tmp]][i];
		else
		{
			q[++tail]=son[tmp][i];
			next[son[tmp][i]]=son[next[tmp]][i];
		} 
}

構建trie圖的程式碼和構建AC自動機有何區別?我們首先會發現程式碼中多了對不存在子的結點情況的判斷,為什麼這樣是對的?如果匹配時,母串的字元不是當前結點的子結點,就需要通過next陣列往上跳,跳到一個有這個字元作為子結點的結點。我們發現,往上跳的操作是大量重複的,很多不同的結點跳過同樣一段路徑,到達同一個終點,卻要被重複計算,為什麼我們不能利用記憶化的思想,把它的終點記下來呢?於是,我們直接令son[tmp][i]=son[next[tmp]][i],就愉快地解決了這個問題。這種被補成完全k叉樹(k為字符集大小)的trie樹,就是之前說的trie圖。因為每一個結點都有子結點,所以while迴圈就根本不會開始。因此,在建好trie圖之後,next陣列就失去了作用,我們直接讓母串在trie圖上一直走向子結點即可,這樣就可以刪掉while迴圈了。