AC自動機的優化及經典例題
自動機是一種用於解決多模式串匹配問題的工具。
模板題:給定個模式串和個母串(由小寫字母組成),將母串中包含模式串的部分變為號。
判斷一個串是不是另一個串的子串,我們首先會想到演算法,但演算法需要逐個處理每一個模式串,太大時顯然會超時。這時,自動機便派上了用場,它的核心也是熟悉的陣列,我們可以把它看做樹上的。首先,我們把所有模式串加入一棵樹中(注意,我們要把樹的根結點設為,原因下面會說),接著,我們通過求出樹上每一個結點的值(的含義和中沒有實質的差別),程式碼如下。
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]; } }
以上程式碼的前兩行看起來有些奇怪,第一行把樹的根結點加入佇列,第二行又設定了一個號結點,並把它的所有子結點都設為。其實,這樣做是為了避免匹配中的一些特殊情況:假設遇到一個樹上無法找到的字元,對於任何一個,都滿足,當時,,程式就會無限迴圈。為了避免這樣的問題,我們把樹的根結點設為,當時,無論取何值,都滿足,從而退出迴圈。建出陣列後,我們開始匹配母串,即讓母串在這棵樹上順著陣列跑,記一個表示當前到達的結點,但還有一個細節要注意:統計答案時,我們不應只統計所在結點的答案,還應統計每一個所在結點順著陣列能到達的結點(只要對於每一個,再開一個,順著陣列跑一跑就行了),否則會出現如下情況:
hack資料:
輸入:
輸出:(漏掉了)
為什麼會出現這樣的情況呢?有些模式串可能是其他模式串的子串,所以會被遺漏...程式碼如下。
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
我做這題時也按照上面記的方法,結果超時了好幾發,後來仔細看看題目,已經保證了一個模式串不可能為其他模式串的子串,因此根本不需要這個變數,只需要直接判斷即可。這道題和上一題有一個區別:刪掉一個模式串後,兩邊剩餘的母串可能會拼出新的模式串,那應該如何操作呢?我一開始想到開一個陣列表示母串中每一個字元的下一個字元,核心程式碼如下。
if(vis[tmp]) to[i-dep[tmp]]=i+1,i=1;
結果又又...的原因有二。一:每次刪除一個模式串,就從頭再開始匹配,但並沒有變為。二:如果刪除一個模式串後,兩邊的母串會拼成一個新的模式串,陣列指向的位置就會出錯,因為沒有相應變化。的原因也顯而易見:每次刪除一個模式串,就從頭再開始匹配,造成了極大的時間浪費,其實我們只要從被刪模式串的前一位繼續匹配即可,但是注意,不能直接把變為,而要開一個陣列記錄匹配到母串的每一位時所在的位置。解決了的問題,如何解決的問題呢?其實只要開一個棧就可以了...棧中的元素不能是字元,而是,否則繼續...匹配程式碼如下。
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]];
}
自動機的重要優化:構建圖。
自動機的複雜度是什麼?這是個值得思考的問題,迴圈中的看起來十分礙事,事實上,它也的確能被某些特殊資料卡到,有沒有什麼辦法去掉中間的迴圈呢?構建圖即可。什麼是圖?程式碼如下。
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];
}
}
構建圖的程式碼和構建自動機有何區別?我們首先會發現程式碼中多了對不存在子的結點情況的判斷,為什麼這樣是對的?如果匹配時,母串的字元不是當前結點的子結點,就需要通過陣列往上跳,跳到一個有這個字元作為子結點的結點。我們發現,往上跳的操作是大量重複的,很多不同的結點跳過同樣一段路徑,到達同一個終點,卻要被重複計算,為什麼我們不能利用記憶化的思想,把它的終點記下來呢?於是,我們直接令,就愉快地解決了這個問題。這種被補成完全叉樹(為字符集大小)的樹,就是之前說的圖。因為每一個結點都有子結點,所以迴圈就根本不會開始。因此,在建好圖之後,陣列就失去了作用,我們直接讓母串在圖上一直走向子結點即可,這樣就可以刪掉迴圈了。