寫在前面
《關於我把后綴樹題數據范圍縮小出成模擬賽題讓 63 級做並說標算只是一個簡單的 Trie 樹這件事是否可行》
個人認為后綴樹是一種很雞肋的數據結構。
它能做的 SAM 都能做,構建后綴樹都可以直接用 SAM = =
但是簡單了解后綴樹可以加深對 SA 和 SAM 的理解,這玩意又很簡單,我建議簡單了解即可。
定義
對於一個字符串 \(S\),它的后綴樹是由其所有后綴 \(S[i:n]\,(1\le i\le n)\) 組成的,經過信息壓縮后的 Trie 樹。
暴力構建
考慮增量法,暴力枚舉原串的每個后綴,將其插入字典樹。
本質不同的子串個數最多有 \(O\left(\frac{n^2}{2}\right)\) 個,故節點數最多可能達到 \(O(n^2)\) 級別。
此時使用后綴樹與直接枚舉原串的子串等價,復雜度為 \(O(n^2)\) 級別。
Ukkonen
可以在線性時間復雜度內構建后綴樹。
構建后綴樹的常用算法,推薦閱讀這篇博文進行學習:炫酷后綴樹魔術 _ EternalAlexander的博客。
虛樹 + SA
雖然節點數可能達到 \(O(n^2)\) 級別,但只有 \(n\) 個后綴,終止狀態數僅有 \(n\) 個。
大部分節點僅有一個孩子,這樣的鏈信息可以合並。考慮建后綴樹的虛樹,縮鏈成邊。
前置知識
簡單介紹虛樹(抄一波課件):
對於樹 \(T=(V,E)\),給定關鍵點集 \(S\subseteq V\),則可定義虛樹 \(T'=(V',E')\)。
對於點集 \(V'\subseteq V\),使得 \(u\in V'\) 當且僅當 \(u\in S\),或 \(\exist x,y\in S,\operatorname{lca}(x,y)=u\)。
對於邊集,\((u,v)\in E'\),當且僅當 \(u,v\in V'\),且 \(u\) 為 \(v\) 在 \(V'\) 中深度最淺的祖先。
個人理解:
僅保留關鍵點及其 lca,縮鏈成邊。
可能刪去一些不包含關鍵點的子樹。壓縮了樹的信息,同時丟失了部分樹的信息。
顯然,一棵后綴樹的關鍵點 即其 \(n\) 個終止狀態。
一個分叉點會合並至少兩個點,當后綴樹為完全二叉樹時虛樹節點數最多,為 \(2n-1\) 個,節點數變為了 \(O(n)\) 級別。
構建方法
假設已知后綴樹的結構。
考慮增量法,每次向虛樹中添加一個關鍵點。
先求得 關鍵節點 的 dfs 序,規定按字典序 dfs,按照 dfs 序添加關鍵節點。
單調棧維護虛樹最右側的鏈(上一個關鍵點與根的鏈),單調棧中節點深度遞增,棧頂一定為上一個關鍵點。
每加入一個關鍵點 \(a_i\),令 \(\operatorname{lca}(a_{i-1},a_i)=w\)。
將棧頂 \(dep_x > dep_w\) 的彈棧,加入 \(w,a_i\),即為新的右鏈。特別地,若棧頂存在 \(dep_x=dep_w\),則不加入 \(w\) 節點。
在此過程中維護每個節點的父節點,在彈棧時進行連邊並維護信息,即得虛樹。
套 SA
但是后綴樹的結構並不已知, 已知還建虛樹干什么 發現上述過程中並沒有用到后綴的性質。
可以總結一下上述建虛樹的過程:
- 求關鍵點的 dfs 序。
- 單調棧維護右鏈。
- 插入關鍵節點,求兩相鄰關鍵點的 \(\operatorname{lca}\),比較深度。
對於前兩步,關鍵點 \(i\) 的 dfs 序即為后綴數組中的 \(sa_i\),可使用 DC3 算法 \(O(n)\) 求得。單調棧的復雜度為 \(O(n)\)。
發現兩關鍵節點 代表排名相鄰的 兩后綴。插入 \(sa_i\) 時,\(1\sim \operatorname{lca}\) 的鏈代表兩后綴的最長公共前綴,即 \(\operatorname{lcp}(sa_{i-1}, sa_i)\)。
顯然 \(\operatorname{lcp}\) 的長度,同時也是 \(\operatorname{lca}\) 的深度,等於 \(\operatorname{height}_i\)。
\(\operatorname{lca}\) 節點是誰不知道,但這並不妨礙彈棧,彈棧過程只關心節點的深度。
彈棧停止后,若棧頂存在 \(dep_x=\operatorname{lcp}\),則 \(\operatorname{lca}\) 已在棧中,直接停止彈棧。否則新建一個 \(dep_x=\operatorname{lcp}\) 的節點插入,當做 \(\operatorname{lca}\) 即可。
使用倍增實現 SA,復雜度 \(O(n\log n)\)。使用 DC3 算法復雜度為 \(O(n)\) 級別。
后綴自動機
SAM 的 parent 樹是反串的后綴樹。
建出反串的 SAM 之后,可以直接得到一棵后綴樹。
時間復雜度為 \(O\left(n\left| \sum\right|\right)\) 或 \(O(n\log n)\)。
與 SA 的關系
SA 可以看作由后綴樹的所有終止狀態按字典序排列得到的數組。
SA 的 \(\operatorname{height}\) 數組可以看做后綴樹中相鄰兩終止狀態的 \(\operatorname{lca}\) 的深度,從這個角度上可以更加形象直觀地理解 SA 的許多性質。
同時,這也證明了后綴樹的適用范圍一定比 SA 大。
奈何 SA 配合 \(\operatorname{height}\) 可以實現大部分后綴樹的功能,又好寫,時間上與后綴樹又幾乎相同,空間又小,所以我喜歡 SA(
做題的時候可以先直觀地從后綴樹的角度出發思考,再用 SA 進行實現。
可以避免抽象的思考過程,比較適合我這樣的無腦選手(
例題
這里簡述使用后綴樹求解的思路,以及如何用后綴樹和 SA 進行實現。
SP705 SUBST1 - New Distinct Substrings
\(T\) 組數據,每次給定一個字符串,求該字符串本質不同的子串數量。
兩個子串本質不同,當且僅當兩個子串長度不等,或長度相等但有任意一位不同。
\(1\le T\le 1\le|s|\le 5\times 10^4\)。
280ms,1.46GB。
一種顯然的做法是建出后綴樹,答案即未壓縮信息的后綴樹中的節點個數。
在縮鏈成邊的同時維護邊中包含的節點個數即可。
總復雜度 \(O(n)\)。
另一種想法是用所有子串的個數 \(\frac{n(n+1)}{2}\) 減去重復子串的個數,顯然重復的串一定出現在某兩個后綴的公共前綴部分。
考慮后綴樹上重復統計部分的位置,有下圖所示:
觀察其中的單色區域的形態,可以考慮增量法統計答案,按照字典序依次將所有后綴加入到后綴樹中。
考慮加入 \(sa_i\) 后,新增的本質不同的子串的數量,顯然即 \(\operatorname{dep}(sa_i) - \operatorname{dep}(\operatorname{lca}(sa_i, sa_{i-1}))\),代表不作為之前加入的,\(sa_i\) 的前綴的數量。
字典序相鄰的節點的 \(lca\) 的深度即為 SA 中的 \(\operatorname{height}\),則最終答案即:
SA 簡單實現即可,總復雜度 \(O(n)\sim O(n\log n)\),依賴於實現。
「AHOI2013」 差異
給定一長度為 \(n\) 的字符串 \(S\),令 \(T_i\) 表示從第 \(i\) 個字符開始的后綴,求:
\[\sum_{1\le i<j\le n}\{\operatorname{len}(T_i) +\operatorname{len}(T_j) - 2\times \operatorname{lcp} (T_i,T_j)\} \]\(\operatorname{len}(a)\) 表示字符串 \(a\) 的長度,\(\operatorname{lcp}(a,b)\) 表示字符串 \(a,b\) 的最長公共前綴長度。
\(1\le n\le 5\times 10^5\)。
1S,512MB。
這個式子玩意長得就很樹上差分。
對於 \(S\) 的后綴樹,\(\operatorname{lcp}\) 即為后綴樹的 \(\operatorname{lca}\),則上式等價於后綴樹上所有后綴之間的距離之和。
則樹上某一點的對答案貢獻,即它的 \(\operatorname{dep}\) 乘上以它為 \(\operatorname{lca}\) 的后綴節點的數量。記錄子樹大小,DP 實現即可。
總復雜度 \(O(n)\) 級別。
寫在最后
參考資料:
OI-wiki 虛樹
利用后綴數組構造后綴樹_AZUI
IOI2004 國家集訓隊論文 后綴數組 許智磊
炫酷后綴樹魔術 _ EternalAlexander的博客