1. 程式人生 > 實用技巧 >字尾陣列(SA)學習筆記

字尾陣列(SA)學習筆記

字尾排序

給定字串 \(a\),我們要將它的 \(n\) 個字尾按字典序排序,形成一個 \(sa\) 陣列,\(sa_i\) 表示排名第 \(i\) 的字尾左端點。順便搞出一個 \(rk=sa^{-1}\)

明確一點,由於各字尾長度不等,所以排序結果是唯一的。

基數排序

字尾排序需要用到。

考慮對一個整數序列 \(x\) 排序。我們考慮將它扔進一個桶裡,\(cnt_i\) 表示值為 \(i\) 的個數。對 \(cnt\) 做一遍字首和得到 \(Cnt_i\),那麼此時 \(Cnt_i\) 表示 \(\leq i\) 的個數。

先令 \(x\) 為任意一個順序。然後從後往前遍歷,將答案序列 \(x'\)

的第 \(Cnt_{x_i}\) 位放上 \(x_i\) 並令 \(Cnt_{x_i}\gets Cnt_{x_i}-1\)。不難發現這個排序演算法是穩定的,複雜度為 \(\mathrm O(n+v)\),其中 \(v\) 是值域大小。

但是有的時候值域過大怎麼辦呢。我們考慮設一個 \(base\),將每個數分成 \(\log_{base}v\) 份,然後從高往低設為關鍵字排序。那麼就歸約成了更強的問題:給定若干關鍵字,排序。由單次基數排序的穩定性,可知從低關鍵字往高排若干遍是正確的。

複雜度 \(\mathrm O(\sum(n+v_i))\),其中 \(v_i\) 是第 \(i\) 關鍵字的值域大小。特殊的,用上述設 \(base\)

方法排整數序列的話,複雜度是 \(\mathrm O((n+base)\log_{base}v)\)。不難發現這種基數排序是嚴格強於桶排的。

基於倍增的字尾排序演算法

假如已知 \(a_{i\sim i+2^k-1}\) 的排序結果,該如何求出 \(a_{i\sim i+2^{k+1}-1}\) 的排序結果。這就有點簡單了吧,將後一半當作第二關鍵字,前一半當作第一關鍵字排序即可。直接 sort 是線性二次對數的,可以改成 gay 排做到線對。

這樣的複雜度在 OI 中基本上可以說足夠了。有更優的線性做法,被卡毒瘤題了再學吧。

高度陣列

定義高度陣列(我也不知道為啥叫這個)\(hi_i\)\(sa_{i-1}\)

\(sa_i\) 這兩個字尾的 lcp。\(i=1\) 時無定義。

求法

引理:\(hi_{rk_i}\geq hi_{rk_{i-1}}-1\)。證明挺簡單的吧,就根據直觀性顯然有,存在一個字尾與字尾 \(i\) 的 lcp 長度 \(\geq hi_{rk_{i-1}}-1\)。結合下面將要說的 LCP Lemma 可知該引理正確性。

根據該引理,顯然有一個按 \(i=1\to n\) 列舉的均攤線性的求法。

給一份 uoj 上模板題程式碼吧,lg 和 loj 都太遜,連 \(hi\) 都不要求。

性質

LCP Lemma:字尾 \(i,j(rk_i<rk_j)\) 的 lcp 為 \(hi_{rk_i+1\sim rk_j}\) 的 RMinQ。這個也很好證明了吧,根據 \(sa\) 陣列的有序性即可輕鬆得到該 Lemma 的正確性,詳細證明留給讀者自己思考。

應用

一般就是根據區間是字尾的字首,按照 \(sa\) 的順序在 \(hi\) 陣列上搞事情吧,或者根據 LCP Lemma 搞事情……其實 SA 這東西套路啥的也是要多刷題才知道的。一般求出陣列們之後就可以轉化為 DS 向的另一道題了。

有心的同學可能發現了,SA 是嚴格強於 Z 的?因為 Z 是求字尾 \(1\)(原串)和其他字尾的 lcp,而 SA 是求任意兩個字尾的 lcp。

講一個非常經典且簡單的應用吧:本質不同子串個數。

我們考慮遍歷 \(sa\) 陣列,由於區間是字尾的字首,每次把當前字尾的不屬於之前遍歷過的字尾的字首的字首給貢獻到答案裡,是個 so-called 容斥。那麼如何算這個重複的呢。顯然有重複的字首集合是一個字首。那麼和 \(sa_{j<i}\) 的重複字首集合顯然大小為 \(j\sim i\) 的那一段 RMQ。它們的並顯然是最大的那個,根據單調性有,重複的數量就是 \(hi_i\)。於是答案是 \(\dfrac{n(n+1)}2-\sum hi_i\)