SAM 學習筆記

2022-09-14 17:42:15 字數 3683 閱讀 2677

詳見 \(\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 被定義為...