AC自動機入門


AC自動機入門

我學的時候看的是yyb的博客
鏈接一個神奇的東西

講之前的bb

PS:不要想着馬上能理解AC自動機,那是不可能的
建議先大致理解一下,然后敲幾次板子,這樣雖然自己心里不爽,但是在敲板子的過程中就會慢慢理解了

一.算法基礎

1.KMP字符串匹配
2.trie樹

要求入門並能有一定技巧地運用

二.由來

(匹配泛指各種字符串之間相互包含,交集等問題)
我們學習了KMP,是用來2個字符串匹配的算法:O(m+n)
現在給出很多個字符串,去把他們和另外一個字符串匹配,如果逐個匹配,顯然會很慢,所以引入一種新算法:AC自動機


這個是分割線吧?

正式開始:

主要思想

  • Q:多個字符串?
    A:我們學了trie樹是吧,預備一下,把要被匹配的字符串全丟進去
  • Q:字符串匹配?
    A:我們學了KMP對吧,想一下KMP的原理,突然發現next[]是不是很吊,預處理出來。
    肯定有用,但是肯定預處理方法不同(見后)
  • Q:這個我也想得到啊!
    A:所以肯定是一個KMP結合trie樹的算法,它就叫AC自動機

實現

注意:以luogu AC自動機模板為例
首先一個圖,自己畫下來再根據代碼模擬手玩更容易理解
來自yyb:

Insert

. 先把所有的匹配字符串丟進trie樹,這個不用多說
ljl[].son[]表示now的每一個兒子(26個字母) 板子
ljl[].end記錄trie樹上在這里結束的是哪個串
PS:代碼里trie數組變成了我自己的名字//滑稽,ljl.son[]==trie[][];

il void Insert()
{
    rg int len=s.length();
    rg int now=0;
    for(rg int i=0;i<len;++i)
    {
        rg int kk=s[i]-'a';
        if(!ljl[now].son[kk])
            ljl[now].son[kk]=++cnt;
        now=ljl[now].son[kk];
    }
    ljl[now].end++;
}

get_fail

.對於匹配,我們考慮找到KMP算法中的next[]數組,但是我們這里把它叫做ljl[].fail,並且也有很多不一樣的地方
聲明一下:fail指的是當前找到的串(在trie樹上找)的最長后綴在哪個地方
PS:根據那個圖來找,自己試試
總結一下幾個地方(現在大可不必理解,背下來先)

  • trie樹上第二層所有的fail都是根節點(0),因為最長后綴肯定沒有地方可以找到,只能是‘空’
  • 我的兒子的fail 是 我的fail的對應兒子(在圖上看一下對不對)
  • 如果沒有想要的兒子,就把我的這個兒子指向我的fail的對應兒子(看后面的板子來理解這句話吧//苦笑)
  • 運用類似bfs的方法來找fail
il void get_fail()
{
    queue<int> Q;
    while(!Q.empty())Q.pop();
    for(rg int i=0;i<26;++i)
    {
        if(ljl[0].son[i])
        {
            ljl[ljl[0].son[i]].fail=0;//1
            Q.push(ljl[0].son[i]);
        }
    }
    while(!Q.empty())
    {
        rg int now=Q.front();Q.pop();
        for(rg int i=0;i<26;++i)
        {
            if(ljl[now].son[i])
            {
                ljl[ljl[now].son[i]].fail=ljl[ljl[now].fail].son[i];//2
                Q.push(ljl[now].son[i]);
            }
            else ljl[now].son[i]=ljl[ljl[now].fail].son[i];	//3			
        }
    }
}

肯定還是無法理解對不對,接下來還有無法理解的東西,我的建議是先記下來板子,敲板子的時候會有驚喜的!!!(我就是這樣理解的)

Query

一句話:Query匹配暴力跳fail:
每匹配一個新的字母,我們暴力跳一遍它所有的fail(先別質疑復雜度),自己模擬一下,知道正確性就ok了……

il int Query(string s)
{
    rg int L=s.length();
    int now=0,ans=0;
    for(rg int i=0;i<L;++i)
    {
        rg int kk=s[i]-'a';
        now=ljl[now].son[kk];//新匹配一個字母
        for(rg int tt=now;tt&&ljl[tt].end!=-1;tt=ljl[tt].fail)
        //暴力跳fail
        {
            ans+=ljl[tt].end;
            ljl[tt].end=-1;//標記走過!
        }
    }
    return ans;
}

如果要全部代碼:戳這里

Query更優版

如果你已經比較理解了或者你有強大的自信心,就往后看,否則,先過了那個板子再說吧……
真的,后面的東西要先理解如何跳fail,我還真不好解釋,所以只能自己去理解
好吧,入正題:
這里以洛谷AC自動機模板2為例題......
你會發現暴力跳fail會被卡成O(n^2),所以考慮優化(會快很多!)
嗯,以下幾點:

  • 對於所有的i,從fail[i]向i連邊,會構成一棵樹
  • 既然我們每次要暴跳一遍,為何不做一次跳?
  • 那么我們考慮把串T在trie樹上標記一下那些單詞可以找到,然后我們把fail數組在一個新圖中連成一棵樹,再dfs一遍記錄答案,這樣我們就把復雜度中的O(m*n)進化成了O(m+n)
  • 代碼實現就很簡單了

PS:我的代碼從ljl[]又變回了trie[][] 習慣一下吧
嗯,代碼是模板2的:AC自動機模板2題解


免責聲明!

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



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