詳細揭秘 SAM
這個咕了好久
這個東西很玄乎,大家抽象理解就好。
首先,SAM 是啥:
-
字尾自動機。
-
每個節點代表一個狀態,裡面存著一些原串的子串(但是不會在程式碼中存)。
-
每個節點有轉移邊指向其他節點,一個字元最多有一條它的轉移邊。
-
整個 SAM 的節點和轉移邊構成 DAG(有向無環圖)。
-
每個節點有它的 link 指標,指向另外一個節點。
-
整個 SAM 的節點和 link 指標構成一棵內向樹。
下面是一些你需要知道的定義:
\(\texttt{endpos}\):設字串 \(t\) 的 \(\texttt{endpos}\) 集合表示它在原串 \(s\) 中的出現位置構成的集合,出現位置以最後一個字元定位。
如 \(s=\texttt{aabaaba}\) 中,串 \(\texttt{ab}\) 的 \(\texttt{endpos}\) 即為 \(\{3,6\}\)。
接著,我們將 \(\texttt{endpos}\) 相同的兩個串劃分到同一個 等價類 中。
例如上面的例子,\(\texttt{aab}\) 和 \(\texttt{ab}\) 就在一個等價類中,這個等價類即為 \(\{\texttt{aab},\texttt{ab}\}\)。
好了。那麼現在我告訴你,SAM 每個節點就代表一個等價類。
為了方便說明,現在我們下一些定義:
- 對於一個等價類(節點)\(u\),設 \(longest(u)\) 表示等價類中最長的字串,\(maxlen(u) = |longest(u)|\)。類似地,設 \(shortest(u)\) 表示等價類中最短的字串,\(minlen(u) = |shortest(u)|\)。
現在有一些顯而易見的結論:
- 同一個等價類中的字串,兩兩之間一定是字尾關係。
因為既然它們 \(\texttt{endpos}\) 相同,從 \(\texttt{endpos}\) 往前延伸,形成的串們一定有某一段字尾相同。
如圖,紅色是 \(\texttt{endpos}\),藍色和綠色是等價類中的兩個串。
進一步地,這些串一定都是等價類中最長串的字尾。
- 兩個串的 \(\texttt{endpos}\) 要麼成包含關係,要麼不相交。
這個顯然。只要有相交說明這兩個串成字尾關係,那麼它們 \(\texttt{endpos}\) 必定是一個包含一個的。(跟上圖一樣)
- 一個等價類 \(u\) 中的串長度連續,覆蓋了區間 \([minlen(u),maxlen(u)]\)。
這個也顯然,\(longest(u)\) 的所有字尾長度在 \([minlen(u),maxlen(u)]\) 之間的,\(\texttt{endpos}\) 至少會包含 \(longest(u)\) 的 \(\texttt{endpos}\),又不會比 \(shortest(u)\) 的 \(\texttt{endpos}\) 多。所以它們全都在一個等價類中。
這意味著一個等價類含有的串數即為 \(maxlen-minlen+1\)。
下面引入 link。
我們定義一個節點(等價類)\(u\) 的 link 指標指向另一個節點(等價類)\(v\),滿足
-
\(longest(v)\) 是 \(longest(u)\) 的真字尾。
-
在所有滿足上述條件的 \(v\) 中,\(maxlen(v)\) 最大。
換句話說,在 \(longest(u)\) 的字尾中,選一個最長的字尾使得它的 \(\texttt{endpos}\) 與 \(u\) 的 \(\texttt{endpos}\) 不同(前者比後者更大),將 \(u\) 的 link 指向這個 \(\texttt{endpos}\) 代表的等價類。
如圖,假設整個字串是 \(longest(u)\),那麼 link 鏈的關係應該如圖所示。
可以看出來一直跳 link 最後會去到空串,對應的也就是初始狀態。
以及把這些等價類的串長區間(也就是圖中黑、紅、藍、綠色的括號)並起來,最後會得到 \([0,maxlen(u)]\)。
這個東西也可以這麼理解:把 \(longest(u)\) 每次刪去頭一個字元,隨著串變短,在 SAM 原串中出現位置自然會變多(即 \(\texttt{endpos}\) 的擴充)。
按照 \(\texttt{endpos}\) 變大的位置我們把這些字尾劃分成很多區間(對應不同的等價類),之間用 link 連線。
然後是節點的轉移邊。
這個東西其實很簡單:假設當前節點為 \(u\),它有一條 \(c\) 的轉移邊指向 \(v\)。
那麼你可以理解為把 \(u\) 中的所有字串後面加上 \(c\) 得到的字串一定在 \(v\) 中。
因此想要找到一個串在 SAM 中屬於的節點,只需要逐個字元地走轉移邊,走到的節點就是了。
然後是 link 樹。
我們知道,整個 SAM 的節點和 link 指標構成一棵內向樹。這是因為每個點不斷跳 link 最後都會回到初始節點。
同時,這棵樹還是 \(\texttt{endpos}\) “要麼成包含關係,要麼不相交”關係構成的樹。
因為跳 link 的同時 \(\texttt{endpos}\) 也會慢慢增加元素,也就是說越靠近根節點 \(\texttt{endpos}\) 越大。
這棵樹有很廣泛的用途,一會再講。
不講如何構造。這個東西有點複雜,但是感性理解也能懂。建議背程式碼
SAM 有什麼用:
- 檢查串 \(t\) 是否在串 \(s\) 中出現過。
根據上面所說的,對 \(s\) 建 SAM,把 \(t\) 放上面按照轉移邊走,能一直走下去就是出現過。
- 求串 \(t\) 在串 \(s\) 中的出現次數。
出現次數就是 \(\texttt{endpos}\) 集合大小。
因此我們對 SAM 的節點預處理 \(siz\) 表示 \(\texttt{endpos}\) 集合大小,找到 \(t\) 對應的節點查詢 \(siz\) 即可。
如何預處理?先給出方法:先將 不由複製得到的 節點的 \(siz\) 設為 \(1\),
對 link 樹 dfs 一遍,對每個點 \(u\) 執行 \(siz_u\gets siz_u+siz_v\) 後得到的就是最終的 \(siz\)。
為什麼這樣是對的?我們考慮每個不由複製得到的節點(下稱真節點)。假設它是在加入第 \(k\) 個字元時建立的,
在建立時它的 \(siz=1\),並會使原串 \(2\sim k\) 的字尾 \(\texttt{endpos}\) 多了一個 \(k\),即 \(siz\) 加 \(1\)。
因為遍歷字尾相當於跳 link,所以這相當於把這個真節點在 link 樹上到根的路徑全部 \(+1\)。
這個和求子樹和的效果是一樣的,與我們用求子樹和的方法最後處理效果一致。
- 求串 \(s\) 的本質不同子串數量。
SAM 已經幫你把相同的子串壓縮在一起了。
我們知道一個節點含有的串數即為 \(maxlen-minlen+1\),我們對所有節點的這個值求和再 \(-1\)(空串)即可。
- 找到串 \(s\) 的一個子串 \(s_{l,r}\) 在 SAM 中對應的節點。
這個非常有用,在一些關於區間的題裡會常用到。(如 CF666E)
子串 \(s_{l,r}\) 就是字首 \(s_{1,r}\) 的一段字尾。
我們考慮對於每個字首先預處理出它在 SAM 中屬於的節點。
然後從字首 \(s_{1,r}\) 屬於的節點開始,不斷跳 link(相當於遍歷該字首的字尾),直到當前 \(maxlen<=r-l+1\)。
由於跳 link 過程中 \(maxlen\) 是不斷減小的,我們可以用倍增代替這個暴力跳的過程。
這樣單次詢問複雜度就做到了 \(\mathcal O(log n)\)。