AC自動機學習筆記-2(Trie圖&&last優化)


我是連月更都做不到的蒟蒻博主QwQ

考慮到我太菜了,考完noip就要退役了,所以我決定還是把博客的倒數第二篇博客給寫了,也算是填了一個坑吧。(最后一篇?當然是悲愴のnoip退役記啦QAQ)

所以我們今天學習的是AC自動機的Trie圖和last優化。如果不知道什么是AC自動機,建議看一看我的上一篇博客:AC自動機學習筆記1

Trie圖

上次我們說到朴素的AC自動機的時間復雜度是布星的,原因如下:

匹配時因為每次都要跳fail邊,復雜度上界可以達到 $ O(ml) $

而Tire圖就是用來解決這種問題的。可以想到,匹配時跳fail邊是十分浪費時間的。舉個例子,對於字符集{a,b,c}上的模式ab,aab,aaab,aaaab,ac和文本串aaaac,它們建出來的AC自動機和匹配過程是這樣的(藍色邊是Trie樹的邊,紅色邊是fail指針,黃色邊是匹配時的狀態轉移):

我們會想,如果失配時可以一步到位就好了。每次跳fail邊的過程是固定的:一直跳,直到找到擁有兒子c的節點為止。也就是說,無論什么時候在這個節點上失配,只要你找的是字符c,你總會在固定的節點上重新開始匹配。既然這樣,不如直接把那個字符為c的節點變成自己的兒子,就可以省去跳fail邊的麻煩:

上圖中,所有的節點的a,b,c三個子節點都是滿的(未畫出的邊都指向根節點,表示完全失配只能從根重新開始)。這樣,原本是DAG結構的AC自動機上出現了環,這樣的結構我們稱之為Trie圖。於是乎,在匹配的時候我們終於可以不用考慮fail邊,一口氣不停地匹配到底辣٩(๑>◡<๑)۶復雜度變成了真正的 $ O(m) $ ,所以你就可以拿這個算法去爆踩std啦qwq

那么,怎么利用fail指針將AC自動機轉化為Trie圖呢?其實,只需要在構建fail指針時順便修改子節點就行了:

void build()
{
    queue<int>q;
    q.push(1);
    while(!q.empty())
    {
        int x=q.front();q.pop();
        for(int i=0;i<26;++i)
        {
            int c=ch[x][i];
            if(!c){ch[x][i]=ch[fail[x]][i];continue;}//關鍵,把子節點改成fail節點的子節點
            q.push(c);
            int fa=fail[x];
            while(fa&&!ch[fa][i])fa=fail[fa];
            fail[c]=ch[fa][i];
        }
    }
}

因為當你遍歷到這個節點時,fail節點的所有兒子肯定已經求出來了,所以直接用fail節點的子節點就好了。

last優化

上述方法將建圖+匹配的復雜度成功優化為了 $ O(\sum n+m) $ ,但是別忘了,匹配成功時的計數也是需要跳fail邊的。然而,為了跳到一個結束節點,我們可能需要中途跳到很多沒用的偽結束節點:

如果一個節點的fail指向一個結尾節點,那么這個點也成為一個(偽)結尾節點。在匹配時,如果遇到結尾節點,就進行相應的計數處理。

這里面就又有優化的余地了:對於不是真正結束節點的偽結束點,直接跳過它就好了。我們用一個last指針表示“在它頂上的fail邊所指向的一串節點中,第一個真正的結束節點”。於是,每次計數處理時,我們不跳fail邊,改為跳last邊,省去了很多冗余操作。

獲得last指針的方法也十分簡單,就是在void build()中加一句話:

last[c]=end[fail[c]]?fail[c]:last[fail[c]];

然后匹配時的代碼就變成了:

void count(int x)
{
    while(x)
    {
       //計數、打印等,視題目要求頂
        x=last[x];
    }
}

void match()
{
    int now=1;
    for(int i=1;s[i]!='\0';++i)
    {
        int x=s[i]-'a';
        now=ch[now][x];
        if(end[now])count(now);
        else if(last[now])count(last[now]);
    }
}

注意:last優化是對復雜度沒有影響的小優化,但是大多數情況下效果明顯,類似於搜索剪枝。

總結

trie圖和last優化都是在“如何跳過不必要的操作”上進行思考后的產物。這種思想可以被運用在很多題目里面,往往可以把復雜度里的一個n給去掉或者變成log。(不存在的。。。所謂“把某種方法完全掌握就可以輕松做出所有這種題”是某C姓教練最喜歡說的話,他認為“沒做出一道要用到某種數據結構的題”的原因是“對某種數據結構的掌握還是不夠熟練”,進而認為最好且明智的解決方法就是“多刷這種數據結構的題以提高熟練度”。這種人實在不好評價,我們還非得聽他的話。。。)

AC自動機學習筆記就告一段落了,寫這樣一篇博客真的很費勁,感謝您的資瓷啦qwq!


免責聲明!

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



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