字尾自動機學習筆記
字尾自動機學習筆記
推薦大佬的 \(blog\)
什麼是字尾自動機
字尾自動機是在一個 \(DAG\) 圖中, 表示出了字尾自動機的所有後綴
顯然字典樹可以解決,但是字典數的節點數是 \(n^2\) 的。
而後綴自動機可以讓節點數量達到線性。
字尾自動機的一個節點代表一個集合,而這個會在後面的集合中提到。
字尾自動機的前置知識
對於一個字串,他在原串中出現了若干次。出現的右端點編號的集合稱為 \(endpos\) 集合。\(e.g.\) 原串為 \(abcab\) 時, \(endpos(ab) = {2,5}\)
現在給出幾個結論 (不難證明)
1. 如果兩個不同子串的 \(endpos\) 集合中有任意一個元素相同,則其中子串一個必然為另一個的字尾
2. 對於任意兩個子串 \(a\) 和 \(b\), \(len_a \le len_b\) 要麼 \(endpos(a) \in endpos(b)\) ,要麼 \(endpos(a) \cap endpos(b) = \emptyset\)
3. 對於 \(endpos\) 相同的子串,我們將它們歸為一個 \(endpos\) 等價類。對於任意一個 \(endpos\) 等價類,將包含在其中的所有子串依長度從大到小排序,則每一個子串的長度均為上一個子串的長度減 \(1\) ,且為上一個子串的字尾 (簡單來說,一個 \(endpos\) 等價類內的串的長度連續)
4. \(endpos\) 等價類個數級別為 \(O(n)\)
這裡要提到一個重要的東西叫做 \(parent\) 樹
一個 \(endpos\)集合, 如果要把他分割成幾個集合, 每一個集合對應一個 \(endpos\) 等價類 (因為 \(endpos\) 等價類如果不是包含關係, 就不能有相同元素。所以只能把一個 \(endpos\) 集合分割成多個 \(endpos\) 集合,然後在其中繼續分割。)
於是這就是一個樹形結構, 稱其為 \(parent\) 樹
以上圖片是一個字串為 \(aababa\) 的例子
在一個 \(endpos\) 等價類 \(a\) 中, 有最長的串, 同時也有最短的串。設該 \(endpos\) 等價類在\(parent\) 樹上的節點的父親為 \(fa(a)\), 設最長的字串長度是 \(len(a)\), 最短的是 \(minlen(a)\), 那麼 \(minlen(fa(a)) + 1= len(a)\)
附上一張在 \(endpos\) 等價類中的最長字串的圖片
這時, 我們的字尾自動機的節點就是這 \(parent\) 樹上的節點!
但是, 字尾自動機的邊不同於 \(parent\) 樹上的邊。
建立一個字尾自動機
先放程式碼
void ins(int x) {
int p = las, now = las = ++tot;
cnt[now] = 1, f[now].len = f[p].len + 1;
for(; p && !f[p].ch[x]; p = f[p].fa) f[p].ch[x] = now;
if(!p) f[now].fa = 1;
else {
int pto = f[p].ch[x];
if(f[pto].len == f[p].len + 1) f[now].fa = pto;
else {
int sp = ++tot;
f[sp] = f[pto], f[sp].len = f[p].len + 1;
f[now].fa = f[pto].fa = sp;
for(; p && f[p].ch[x] == pto; p = f[p].fa) f[p].ch[x] = sp;
}
}
}
如果在原來的字尾自動機中增加一個字元 \(x\), 那麼原串的字尾要增加一個字元 \(x\)
他出現的次數為 \(1\), 而且他的最長字串是原來的字尾長度 \(+1\)
於是有程式碼
int p = las, now = las = ++tot; // p 是原來的最長字尾,now是現在的最長字尾,las是記錄上次最長字尾的
cnt[now] = 1, f[now].len = f[p].len + 1; // len 同上面的描述,是最長字串長度, f是表示parent樹的一個endpos等價類的集合, 一個字尾自動機的節點
然後包含之原來的 \(las\) 的節點都被其 \(parent\) 樹上的父親節點所包含, 因此如果在他們的後面再新增一個字元 \(x\), 都會跳到該節點。但是如果他的父親已經有轉移了, 那麼就不用再增加這條邊了。而且如果遇到一個節點已經有字元 \(x\) 的轉移邊了, 他的父親一定也有字元 \(x\) 的轉移邊, 所以就不用繼續往上跳了。
程式碼:
for(; p && !f[p].ch[x]; p = f[p].fa) f[p].ch[x] = now;
考慮如何在\(parent\) 樹上處理 \(now\) 的父親指標。
如果一直往上跳的過程中, 所有節點都沒有向字元 \(x\) 的轉移邊, 就說明字元 \(x\) 是第一次出現(因為根結點也沒有字元 \(x\) 的轉移邊), 所以除了根結點沒有集合能夠包含集合編號為 \(now\) 的節點了, 於是其父親指標指向 \(1\)
if(!p) f[now].fa = 1;
否則 \(p\) 就在有轉移邊 \(x\) 的祖先節點上停下來了。
這時 \(p\) 是原來 \(las\) 的字尾, 都加上一個字元 \(x\) 之後, \(pto\) (假設點 \(p\) 加上一個 \(c\) 得到了 \(pto\)) 也是 \(now\)的字尾。 都是如果該節點 \(p\) 增加一個 \(x\) 得到的最長字尾恰好是原來字尾的最長字尾的長度\(+1\), 那麼就說明了再這個節點代表的所有字串中增加一個 \(x\) 後到達 \(p\) 的 \(endpos\) 一致, 而 \(p\) 又是跳父親時第一個遇見的節點, 所以 \(pto\) 一定是 \(now\) 的最長字尾, \(now\) 在 \(parent\) 樹上的父親就是 \(pto\)。
int pto = f[p].ch[x];
if(f[pto].len == f[p].len + 1) f[now].fa = pto;
那麼 \(len(p) > len(pto) + 1\) 怎麼辦?
說明在 \(p\) 中還有另外一個