淺談後綴數組

1. 概述

後綴數組是一種解決字符串問題的有力工具。相比於後綴樹,它更易於實現且佔用內存更少。在實際應用中,後綴數組經常用於解決字符串有關的複雜問題。

本文大部分內容摘自參考資料[1][2]。

2. 後綴數組

2.1   幾個概念

(1)後綴數組SA 是一個一維數組,它保存1..n 的某個排列SA[1],SA[2],……,SA[n],並且保證Suffix(SA[i]) < Suffix(SA[i+1]),1≤i<n。也就是將S 的n 個後綴從小到大進行排序之後把排好序的後綴的開頭位置順次放入SA 中。其中,suffix(i)表示字符串s[i,i+1…n-1],即字符串s起始於第i個字符的後綴。

(2)名次數組Rank[i]保存的是Suffix(i)在所有後綴中從小到大排列的“名次”。

簡單的說,後綴數組是“排第幾的是誰?”,名次數組是“你排第幾?”。容易看出,後綴數組和名次數組爲互逆運算。

(3)height 數組:定義height[i]=suffix(SA[i-1])和suffix(SA[i])的最長公共前綴,也就是排名相鄰的兩個後綴的最長公共前綴。

(4) h[i]=height[rank[i]],也就是suffix(i)和在它前一名的後綴的最長公共前綴。

(5)LCP(i,j):對正整數i,j 定義LCP(i,j)=lcp(Suffix(SA[i]),Suffix(SA[j]),其中i,j 均爲1 至n 的整數。LCP(i,j)也就是後綴數組中第i 個和第j 個後綴的最長公共前綴的長度。其中,函數lcp(u,v)=max{i|u=v},也就是從頭開始順次比較u 和v 的對應字符,對應字符持續相等的最大位置,稱爲這兩個字符串的最長公共前綴。

2.2   幾個性質

(1)LCP(i,j)=min{height[k]|i+1≤k≤j},也就是說,計算LCP(i,j)等同於詢問一維數組height 中下標在i+1 到j 範圍內的所有元素的最小值。

證明略。

(2)對於i>1 且Rank[i]>1,一定有h[i]≥h[i-1]-1。

證明:設suffix(k)是排在suffix(i-1)前一名的後綴,則它們的最長公共前綴是h[i-1]。那麼suffix(k+1)將排在suffix(i)的前面(這裏要求h[i-1]>1,如果h[i-1]≤1,原式顯然成立)並且suffix(k+1)和suffix(i)的最長公共前綴是h[i-1]-1,所以suffix(i)和在它前一名的後綴的最長公共前綴至少是h[i-1]-1。按照h[1],h[2],……,h[n]的順序計算,並利用h 數組的性質,時間複雜度可以降爲O(n)。

3. 後綴數組實現

本節給出高效計算SA,Rank,height和h的算法

(1) 計算名次數組Rank與後綴數組SA

採用倍增算法,先求出名次Rank,然後在O(n)時間內求得後綴數組SA。用倍增的方法對每個字符開始的長度爲2^k 的子字符串進行排序,求出排名,即rank 值。k 從0 開始,每次加1,當2k 大於n 以後,每個字符開始的長度爲2^k 的子字符串便相當於所有的後綴。並且這些子字符串都一定已經比較出大小,即rank 值中沒有相同的值,那麼此時的rank 值就是最後的結果。每一次排序都利用上次長度爲2^(k-1) 的字符串的rank 值,那麼長度爲2^k 的字符串就可以用兩個長度爲2^(k-1) 的字符串的排名作爲關鍵字表示,然後進行基數排序,便得出了長度爲2k 的字符串的rank 值。以字符串“aabaaaab”爲例,整個過程如下圖所示。其中x、y 是表示長度爲2k 的字符串的兩個關鍵字。

(2) 計算數組h

可以令i從1 循環到n按照如下方法依次算出h[i]:

若 Rank[i]=1,則h[i]=0。字符比較次數爲0。

若 i=1 或者h[i-1]≤1,則直接將Suffix(i)和Suffix(Rank[i]-1)從第一個字符開始依次比較直到有字符不相同,由此計算出h[i]。字符比較次數爲h[i]+1,不超過h[i]-h[i-1]+2。

否則,說明i>1,Rank[i]>1,h[i-1]>1,根據性質2,Suffix(i)和Suffix(Rank[i]-1)至少有前h[i-1]-1 個字符是相同的,於是字符比較可以從h[i-1]開始,直到某個字符不相同,由此計算出h[i]。字符比較次數爲h[i]-h[i-1]+2。

可求得最後算法複雜度爲O(n)。

4. 後綴數組應用

4.1 單個字符串相關問題

(1) 可重疊最長重複子串。給定一個字符串,求最長重複子串,這兩個子串可以重疊。

『解析』只需要求height 數組裏的最大值即可。

(2) 不可重疊最長重複子串。給定一個字符串,求最長重複子串,這兩個子串不能重疊。

『解析』先二分答案,把題目變成判定性問題:判斷是否存在兩個長度爲k 的子串是相同的,且不重疊。解決這個問題的關鍵還是利用height 數組。把排序後的後綴分成若干組,其中每組的後綴之間的height 值都不小於k。例如,字符串爲“aabaaaab”,當k=2 時,後綴分成了4 組:

容易看出,有希望成爲最長公共前綴不小於k 的兩個後綴一定在同一組。然後對於每組後綴,只須判斷每個後綴的sa 值的最大值和最小值之差是否不小於k。如果有一組滿足,則說明存在,否則不存在。整個做法的時間複雜度爲O(nlogn)。

(3) 可重疊的k 次最長重複子串。給定一個字符串,求至少出現k 次的最長重複子串,這k 個子串可以重疊。

『解析』 先二分答案,然後將後綴分成若干組。不同的是,這裏要判斷的是有沒有一個組的後綴個數不小於k。如果有,那麼存在k 個相同的子串滿足條件,否則不存在。這個做法的時間複雜度爲O(nlogn)。

(4) 最長迴文子串。給定一個字符串,求最長迴文子串。

『解析』 將整個字符串反過來寫在原字符串後面,中間用一個特殊的字符隔開。這樣就把問題變爲了求這個新的字符串的某兩個後綴的最長公共前綴。

(5) 連續重複子串。給定一個字符串L,已知這個字符串是由某個字符串S 重複R 次而得到的,求R 的最大值。

『解析』窮舉字符串S 的長度k,然後判斷是否滿足。判斷的時候,先看字符串L 的長度能否被k 整除,再看suffix(1)和suffix(k+1)的最長公共前綴是否等於n-k。在詢問最長公共前綴的時候,suffix(1)是固定的,所以RMQ問題沒有必要做所有的預處理, 只需求出height 數組中的每一個數到height[rank[1]]之間的最小值即可。整個做法的時間複雜度爲O(n)。

(6) 重複次數最多的連續重複子串。給定一個字符串,求重複次數最多的連續重複子串。

『解析』先窮舉長度L,然後求長度爲L 的子串最多能連續出現幾次。首先連續出現1 次是肯定可以的,所以這裏只考慮至少2 次的情況。假設在原字符串中連續出現2 次,記這個子字符串爲S,那麼S 肯定包括了字符r[0], r[L], r[L*2],r[L*3], ……中的某相鄰的兩個。所以只須看字符r[L*i]和r[L*(i+1)]往前和往後各能匹配到多遠,記這個總長度爲K,那麼這裏連續出現了K/L+1 次。最後看最大值是多少。

窮舉長度L 的時間是n,每次計算的時間是n/L。所以整個做法的時間複雜度是O(n/1+n/2+n/3+……+n/n)=O(nlogn)。

4.2 兩個字符串相關問題

(1) 最長公共子串。給定兩個字符串A 和B,求最長公共子串。

『解析』先將第二個字符串寫在第一個字符串後面,中間用一個沒有出現過的字符隔開,再求這個新的字符串的後綴數組。當suffix(sa[i-1]) 和suffix(sa[i])不是同一個字符串中的兩個後綴時,max{height[i]}纔是滿足條件

(2) 長度不小於k 的公共子串的個數。給定兩個字符串A 和B,求長度不小於k 的公共子串的個數(可以相同)。

『解析』基本思路是計算A 的所有後綴和B 的所有後綴之間的最長公共前綴的長度,把最長公共前綴長度不小於k 的部分全部加起來。先將兩個字符串連起來,中間用一個沒有出現過的字符隔開。按height 值分組後,接下來的工作便是快速的統計每組中後綴之間的最長公共前綴之和。掃描一遍,每遇到一個B 的後綴就統計與前面的A 的後綴能產生多少個長度不小於k 的公共子串,這裏A 的後綴需要用一個單調的棧來高效的維護。然後對A 也這樣做一次。

4.3 多個字符串相關問題

(1) 不小於k 個字符串中的最長子串。給定n 個字符串,求出現在不小於k 個字符串中的最長子串。

『解析』將n 個字符串連起來,中間用不相同的且沒有出現在字符串中的字符隔開,求後綴數組。然後二分答案:將後綴分成若干組,判斷每組的後綴是否出現在不小於k 個的原串中。這個做法的時間複雜度爲O(nlogn)。

(2) 每個字符串至少出現兩次且不重疊的最長子串。給定n 個字符串,求在每個字符串中至少出現兩次且不重疊的最長子串。

『解析』做法和上題大同小異,也是先將n 個字符串連起來,中間用不相同的且沒有出現在字符串中的字符隔開,求後綴數組。然後二分答案,再將後綴分組。判斷的時候,要看是否有一組後綴在每個原來的字符串中至少出現兩次,並且在每個原來的字符串中,後綴的起始位置的最大值與最小值之差是否不小於當前答案(判斷能否做到不重疊,如果題目中沒有不重疊的要求,那麼不用做此判斷)。這個做法的時間複雜度爲O(nlogn)。

(3) 出現或反轉後出現在每個字符串中的最長子串。給定n 個字符串,求出現或反轉後出現在每個字符串中的最長子串。

『解析』這題不同的地方在於要判斷是否在反轉後的字符串中出現。其實這並沒有加大題目的難度。只需要先將每個字符串都反過來寫一遍,中間用一個互不相同的且沒有出現在字符串中的字符隔開,再將n 個字符串全部連起來,中間也是用一個互不相同的且沒有出現在字符串中的字符隔開,求後綴數組。然後二分答案,再將後綴分組。判斷的時候,要看是否有一組後綴在每個原來的字符串或反轉後的字符串中出現。這個做法的時間複雜度爲O(nlogn)。

5. 總結

5.1 常見問題一句話題解

例1:最長公共前綴

    給定一個串,求任意兩個後綴的最長公共前綴。

解:先根據rank確定這兩個後綴的排名i和j(i<j),在height數組i+1和j之間尋找最小值。(可以用rmq優化)

 

例2:最長重複子串(不重疊)(poj1743)

解:二分長度,根據長度len分組,若某組裏SA的最大值與最小值的差>=len,則說明存在長度爲len的不重疊的重複子串。

 

例3:最長重複子串(可重疊)

解:height數組裏的最大值。這個問題等價於求兩個後綴之間的最長公共前綴。

 

例4:至少重複k次的最長子串(可重疊)(poj3261)

解:二分長度,根據長度len分組,若某組裏的個數>=k,則說明存在長度爲len的至少重複k次子串。

 

例5:最長迴文子串(ural1297)

    給定一個串,對於它的某個子串,正過來寫和反過來寫一樣,稱爲迴文子串。

解:枚舉每一位,計算以這個位爲中心的的最長迴文子串(注意串長要分奇數和偶數考慮)。將整個字符串反轉寫在原字符串後面,中間用$分隔。這樣把問題轉化爲求某兩個後綴的最長公共前綴。

 

例6:最長公共子串(poj2774)

    給定兩個字符串s1和s2,求出s1和s2的最長公共子串。

解:將s2連接到s1後,中間用$分隔開。這樣就轉化爲求兩個後綴的最長公共前綴,注意不是height裏的最大值,是要滿足sa[i-1]和sa[i]不能同時屬於s1或者s2。

 

例7:長度不小於k的公共子串的個數(poj3415)

    給定兩個字符串s1和s2,求出s1和s2的長度不小於k的公共子串的個數(可以相同)。

解:將兩個字符串連接,中間用$分隔開。掃描一遍,每遇到一個s2的後綴就統計與前面的s1的後綴能產生多少個長度不小於k的公共子串,這裏s1的後綴需要用單調棧來維護。然後對s1也這樣做一次。

 

例8:至少出現在k個串中的最長子串(poj3294)

    給定n個字符串,求至少出現在n個串中k個的最長子串。

將n個字符串連接起來,中間用$分隔開。二分長度,根據長度len分組,判斷每組的後綴是否出現在不小於k個原串中。


後綴數組實際上可以看作後綴樹的所有葉結點按照從左到右的次序排列放入數組中形成的,所以後綴數組的用途不可能超出後綴樹的範圍。甚至可以說,如果不配合LCP,後綴數組的應用範圍是很狹窄的。但是LCP 函數配合下的後綴數組就非常強大,可以完成大多數後綴樹所能完成的任務,因爲LCP 函數實際上給出了任意兩個葉子結點的最近公共祖先,這方面的內容大家可以自行研究。

6. 參考資料

(1) 許智磊,IOI2004 國家集訓隊論文《後綴數組》

(2) 羅穗騫,IOI2004 國家集訓隊論文《後綴數組—處理字符串的有力工具》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章