詳見 \(\text\)。
字串 \(s\) 的 \(\text\) 是乙個接受 \(s\) 所有字尾的最小dfa(確定性有限自動機或確定性有限狀態自動機)。其中,\(s\) 每個字尾均可用一條從初始狀態 \(t_\) 到某個終止狀態的路徑構成。
包含 \(s\) 的所有子串:從 \(t_\) 開始的任意路徑都構成乙個子串,每個子串也對應某條路徑。但是到某個狀態的路徑可能不止一條,即每個狀態都對應一些字串組成的集合。
重要概念:結束位置endpos
對於 \(s\) 任意非空子串 \(t\),\(\text(t)\) 為 \(s\) 中 \(t\) 的所有結束位置。根據 \(\text\) 集合將 \(t\) 分為若干個等價類,每個等價類在 \(\text\) 上對應乙個狀態,即 \(\text\) 狀態數為 \(\text\) 等價類個數加 \(1\)(還有初始狀態)。下面是由 \(\text\) 得到的一些性質。
證明都比較顯然。
設 \(w\) 為狀態 \(v\) 對應的 \(\text\) 等價類中最短的字串,\(t\) 是 \(w\) 的字尾且 \(\lvert t\rvert = \lvert w\rvert -1\),則 \(\text(v)\) 連線到 \(t\) 所屬的 \(\text\) 等價類對應的狀態。\(\text\) 構成一棵根節點為 \(t_\) 的樹。
有 \(\text(u)\subsetneq \textu))\),否則 \(u\) 和 \(\text(u)\) 應該合併,那麼 \(\text\) 集合構成的樹和 \(\text\) 是等價的,這就是 \(\text\) 樹,\(\text(u)\) 對應 \(u\) 在樹上的父親。從 \(u\) 開始向上跳父親直到根節點 \(t_\),所有狀態的字串長度區間互不相交但連續。
此處再定義 \(\text(u)\) 表示狀態 \(u\) 中最長字串的長度。
此部分內容暫時咕咕咕。
struct sam
inline void extend(int c)
//沒找到 p 就把 now 連到根節點上
int q=ch[p][c];
//x 為 s 的字尾,現在加入 x+c,且 x+c 已經出現過了,那麼 link(now) 應該連線到最長字串恰好是 x+c 的狀態,即 len(q)=len(p)+1,如果不存在這樣的 q,再分類討論
if(len[q]==len[p]+1) link[now]=q;
else
(x)\) 的字串 \(a\);對於節點 \(y\),長度為 \(\text(y)\) 的字串 \(b\)。如果 \(x\) 是 \(y\) 的祖先,那麼 \(a\) 是 \(b\) 的字尾。
兩個字首 \([1,x]\) 和 \([1,y]\) 的最長公共字尾對應的字串是 \(v_\) 和 \(v_\) 在 \(\text\) 樹上的 \(\text\) 對應的字串。
本質不同子串總數:\(\sum\limits_ \text(i)-\text(i))\),例題 生成魔咒,每次增加 \(\text(now)-\text(now))\) 的貢獻即可。
例題 【模板】字尾自動機 (sam)
求最大值:出現次數大於 \(1\) 某個子串的出現次數乘上長度
建出 \(\text\),\(\text\) 樹上非複製的點都對應了乙個字串。
由於 \(\text\) 集合的性質,點 \(x\) 的子樹內的節點對應的 \(\text\) 集合無交集但都是包含於 \(x\)。所以求一遍子樹和就可以得到點 \(x\) 的 \(size\)。那麼對 \(size\not=1\) 的點 \(x\) 對應的 \(len\times size\) 取乙個最大值即可。
void dfs(int x)
if(book[x]) siz[x]++;
}
例題 lcs - longest common substring
\(n\) 個串的最長公共子串。
首先考慮兩個串的情況。對於第乙個串建出 \(\text\),第二個串在上面匹配。子串可以表示成一段字首的字尾,所以考慮每加入乙個字元,計算新的答案。設以 \(i\) 結尾的最長公共子串長度為 \(now\),當前狀態為 \(v\),新加入字元 \(c_\):
對於多個串也是類似。記 \(tag[i][j]\) 表示第 \(i\) 個串(\(2\leq i \leq n\))在狀態 \(j\) 匹配的最長字串,再對每個狀態的所有 \(tag\) 取個 \(\min\) 得到每個狀態上的答案,最後對所有狀態取 \(\max\) 即可。
注意 \(v\rightarrow \text(v)\) 時,也要更新 \(tag\),這是因為乙個節點 \(x\) 能匹配,那麼 \(x\) 在 \(\text\) 樹上所有祖先都能被匹配。
while(scanf("%s",s[++n]+1)!=eof) len[n]=strlen(s[n]+1); n--;
for(ri int i=1;i<=len[1];i++) a.extend(s[1][i]-'a');
for(ri int i=2;i<=n;i++)
}for(ri int i=2;i<=a.tot;i++)
printf("%d\n",ans);
例題 sublex - lexicographical substring search
本質不同排名第 \(k\) 小子串。
即 \(\text\) 上字典序第 \(k\) 小路徑。乙個想法是求出每個狀態對應的總路徑數,這樣就可以從根節點暴力找到第 \(k\) 小路徑。
對 \(\text\) 上狀態構成的 \(\text\) 進行拓撲排序:
int id[n],cs[n];
//基數排序
inline void topo()
然後根據拓撲序加入狀態,很方便地得到每個狀態的總路徑數。
string ot="";
while(1)
}if(!***||!k) break;
}if(k) puts("");
else cout《例題 [ahoi2013]差異
兩兩字尾的最長公共字首之和,即兩兩字首的最長公共字尾之和。
在 \(\text\) 樹上這有很好的性質,因為兩個字首分別對應的狀態的 \(\text\) 就是它們的最長公共字尾。
所以考慮每個狀態作為 \(\text\) 時的貢獻即可。
for(ri int i=head[x];i;i=e[i].nxt)
例題 2015 集訓隊**集 《字尾自動機及其應用》 例題一 字串
給定 \(n\) 個字串,詢問每個字串有多少非空子串是所有 \(n\) 個字串中至少 \(k\) 個的子串。
將 \(n\) 個串用#
相拼接之後建出 \(\text\),每個串在上面匹配,則只需要考慮每個狀態出現在多少個字串中即可。這裡可以用暴跳 \(parent\) 樹的 \(o(n\sqrt n)\) 做法,或者用資料結構維護做到 \(o(n\text(\log n))\)。具體實現可以參考 \(\text\) 以及**中的題解。
例題 2015 集訓隊**集 《字尾自動機及其應用》 例題二 [apio2014]回文串
求字串 \(s\) 的所有回文子串中,出現次數乘長度的最大值。
\(\text\) 板題,但是 \(\text\) 也可以做,詳見 link。我也會寫一篇較短的題解,但不會放在這篇文章中。
學習筆記 SAM
不想學博弈論不想學 sa 不想學插頭 dp,學 lct 被 axdea d 飛了,那就來學 sam。sam 是字尾自動機,名義上是字尾,但實際上它能表示出乙個字串的所有不同子串。不同於你的 o n 2 列舉,sam 構造,節點和邊的數量也都是 o n 級別的。更具體的,sam 表現為一張 dag,每...
SAM初學筆記
我第一次學習sam,可能有很多地方理解有偏差或者錯誤。還請各位大佬指正。這篇文章中沒有嚴謹的證明,只有感性的理解。追求嚴謹的請轉他處。首先我們定義parent樹 乙個節點與其所代表的字串的最長的,且出現次數與其不一樣的字尾連邊,所形成的樹是parent樹。可以發現parent樹有很多優秀的性質,比如...
SAM學習小記
只是乙個小記,不是演算法詳解 參考資料 史上最通俗的字尾自動機詳解 廣義sam模板題解 簡單的,乙個有向無環圖,邊有字母,滿足起點開始的每一條路徑都是原串的乙個子串。並且保證複雜度在o n o n o n 級別內的。每乙個子串p pp的end pos p endpos p endpos p 被定義為...