學習筆記 字尾自動機

2022-05-23 21:18:09 字數 3567 閱讀 5161

其實我字尾自動機在 \(2020/2\) 的時候就會了,刷了很多題,但是一直沒有搞懂原理,現在補一發關於字尾自動機原理的部落格。我盡量節約時間,把最重要的東西寫的盡量好理解。

我是看這位巨佬的部落格學的,所以會有一些重合的地方,但是我會按照我自己的理解寫(仔細看你會發現我們的表述相差很大),爭取達到乙個優化的效果。

這才是究極大暴力,學會了之後隨便切很多字串毒瘤題。

他主要幹的事情是維護乙個字串中的所有子串,但是基礎複雜度卻能保證是 \(o(n)\) ,此外還有很多優美的性質 \(....\) 原諒我現在沒有辦法一一向你介紹,具體可以看最後的部分字尾自動機應用

中心思想是 \(\tt endpos\) ,也就是維護子串的出現結束位置集合

\(\tt endpos\) 有很多優美的性質,且看我一一向你介紹。

結論1:\(\tt endpos\) 相同的兩個字串 \(a,b\;(|a|\leq |b|)\) ,滿足 \(a\) 一定是 \(b\) 的字尾。

這麼明顯的結論為什麼不感性理解呢?

證明的話考慮他們 \(\tt endpos\) 完全相同,那麼 \(b\) 出現的話一定會連帶著 \(a\) 出現。對於每個結束位置都如此,不難發現 \(a\) 是 \(b\) 的字尾。

現在你應該能感知到為什麼要叫字尾自動機,因為 \(\tt endpos\) 會連帶著一些關於字尾的性質

結論2:對於兩個字串 \(a,b\;(|a|\leq |b|)\) ,那麼要麼 \(\tt endpos(b)\subseteq endpos(a)\) ,要麼 \(\tt endpos(a)\cap endpos(b)=\empty\)

其實也很好感性理解呀!

我們就要證明不會出現相交的情況,使用反證法,假設出現了相交。那麼 \(b\) 出現的某些地方會出現 \(a\) ,\(b\) 出現的另一些地方又不會出現 \(a\) ,顯然矛盾,所以只會有包含或者是不交的情況。

結論3:對於 \(\tt endpos\) 相同的子串,我們將其歸為乙個 \(\tt endpos\) 等價類,對於每乙個等價類按長度排序,每乙個子串的長度都等於上乙個字串的長度\(+1\)

好顯然啊!

我們假設這個等價類中最長的子串是 \(len\) ,那麼最短的子串是 \(jzm\) ,顯然 \(jzm\) 是 \(len\) 的乙個字尾。那麼既然 \(jzm\) 都可以被劃分到這個等價類中,所以長度在他們中間的一定都會劃分到這個等價類中。

有了這個結論,我們可以用 \(len(i)\) 表示第 \(i\) 個 \(\tt endpos\) 等價類的很多資訊了,也就是我們只需要維護等價類中最長的子串

結論4:\(\tt endpos\) 等價類的個數為 \(o(n)\)

這個結論是字尾自動機複雜度得到保證的關鍵,我不敢再說顯然了

我們考慮用樹形關係來表示 \(\tt endpos\) 之間的聯絡,如果 \(i\) 等價類是 \(j\) 等價類的父親,那麼 \(i\) 等價類的所有子串都是 \(j\) 等價類所有子串的字尾。而因為結論 \(2\),我們知道 \(i\) 的所有兒子 \(j\) 所表示的 \(\tt endpos\) 集合一定是不交的。

這樣可以用一種劃分關係來理解這個樹形結構,也就是我們會把 \(i\) 的 \(\tt endpos\) 劃分分給他的兒子。一共只會劃分 \(n-1\) 次就可以到達底層,而 \(1\) 個點的作用相當於劃分了一次,所以點數 \(o(n)\)

我們把這樣的樹形關係叫做 \(\tt parent\;tree\) ,這棵樹就是字尾自動機的關鍵。

上面的結論主要是說明了字尾自動機的可行性和一些關鍵特徵,現在我們來解決具體的構建。

字尾自動機之所以叫自動機,是因為他也有所謂的轉移,就像 \(\tt ac\) 自動機一樣。

字尾自動機的轉移是這個意思:在這個點的子串後面加上乙個字元 \(c\) 所能到達的點(子串的長度要盡量短)。如果我們能構造出轉移,那麼這個自動機就很好用了。現在我們先來看**吧,我會逐一講解**:

void add(int c)

}}

我們乙個乙個加入字元,所以我們處理的是原串字首的字尾自動機。但是現在我們叫加入 \(c\) 之前的字串為原串,加入 \(c\) 之後的串為新串,那麼 \(np\) 其實是整個新串對應的節點,要幹的事有兩個:把 \(np\) 連進圖中 \(/\) 原串字尾加上 \(c\) 之後的 \(\tt endpos\) 可能變化,所以這也要修改一下。

int p=last,np=last=++cnt;val[cnt]=1;

a[np].len=a[p].len+1;

這兩句話就是基本的定義,很顯然,沒有什麼要講的。

for(;p && !a[p].ch[c];p=a[p].fa) a[p].ch[c]=np;

if(!p) a[np].fa=1;

這一部分就是修改 \(p\) 和他的祖先關於 \(c\) 的轉移,如果他們沒有東西可以轉移,那麼轉移到 \(np\) 就是極好的(其實結合轉移的定義就不難理解了),下一句話是如果 \(1\)(根)都沒有 \(c\) 可以轉移,那麼顯然 \(c\) 是在原串中沒有出現過的,所以 \(np\) 直接成為 \(1\) 的兒子。

int q=a[p].ch[c];

if(a[q].len==a[p].len+1) a[np].fa=q;

現在的 \(p\) 已經是第乙個有轉移的祖先了,我們先拿到 \(p\) 的轉移 \(q\),那麼如果 \(len[q]=len[p]+1\) ,那麼說明 \(q\) 一定是 \(p\) 的最長子串後面再接上乙個 \(c\) 的結果,這簡直就是無縫連線啊!然後不難看出 \(q\) 一定是 \(np\) 的字尾,所以把 \(np\) 的父親設定為 \(q\) 是 \(\tt make\; sense\) 的。

int nq=++cnt;

a[nq]=a[q];a[nq].len=a[p].len+1;

a[q].fa=a[np].fa=nq;

for(;p && a[p].ch[c]==q;p=a[p].fa) a[p].ch[c]=nq;

這種情況就是 \(len[q]>len[p]+1\) ,也就是 \(q\) 是 \(p\) 的最長子串後面再加上若干字元。這種情況不能直接設定父親,因為加上若干字元之後就不滿足了字尾關係。那麼我們考慮建乙個新點 \(nq\) ,那麼 \(len[nq]=len[p]+1\),也就是它代表了 \(p\) 代表的字串直接接上 \(c\) 字元的結果。\(nq\) 相當於把乙個子串拆出來

那麼現在就滿足字尾關係了,\(q\) 和 \(np\) 的父親都可以直接賦值為 \(nq\)

現在也不要忘了更新轉移喲,對於 \(p\) 以及 \(p\) 的祖先中轉移是到 \(p\) 點的話,現在轉移就要改成 \(nq\) 點了。結合轉移的定義就知道我們要找的是長度最小的點,所以肯定轉移到 \(nq\) 啊。

現在進入喜聞樂見的複雜度證明環節:由於邊數和點數都是 \(o(n)\) 的,觀察所有的操作發現時間複雜度 \(o(n)\)

我現在做這道題發現有手就行:字串問題

會慢慢補充的 \(....\)

字尾自動機 筆記

參考了hihocoder和clj的課件,看了看hzwer的 懂了些東西,記一下。字尾自動機是一棵trie樹。給出乙個字串s,對於s的乙個子串s,right s 代表乙個集合,為s在s中所有出現的結束位置集合。以s aabbabd 為例,right ab 因為 ab 一共出現了2次,結束位置分別是3和...

字尾自動機學習

今天終於把這週的坑填了,同樣看了很多部落格,這裡就不詳細總結了,就簡單整理一下了。應用 1 存在性查詢 給定文字t,詢問格式如下 給定字串p,問p是否是t的子串。直接按著路徑走,看是否存在即可 2 不同的子串個數 對於每乙個節點即為 len i len fa i 加和即可 3 不同子串的總長 這裡我...

字尾自動機 SAM 學習筆記

參考資料 hihocoder1441 hihocoder1445 史上最通俗的字尾自動機詳解 練習題hihocoder1449 hihocoder1457 hihocoder1465 hihocoder1413 筆記 字串 aab 模板struct suffixautomaton int q ch ...