后綴數組


什么是后綴數組

后綴樹(Suffix tree)是一種數據結構,能快速解決很多關於字符串的問題,缺點是算法復雜難懂且容易出錯。

而后綴數組、后綴自動機、后綴仙人掌都是后綴樹的替代品。

后綴數組 Suffix Array 是一個一維數組,它將字符串S的n個后綴從小到大排序后把排好序的后綴的開頭位置順次放入數組中。

它可以由倍增算法在O(nlogn)的時間內構造出來。

 

后綴數組的基本概念

 

子串

字符串 S 的子串 r[i..j],i≤j,表示 r 串中從 i 到 j 這一段,也就是順次排列 r[i],r[i+1],...,r[j]形成的字符串。

 

后綴

后綴是指從某個位置 i 開始到整個串末尾結束的一個特殊子串。

字符串 r 的從第 i 個字符開始的后綴表示為 Suffix(i),也就是 Suffix(i) = r[i..len(r)]。

 

字符串的大小比較

關於字符串的大小比較,是指通常所說的 “ 字典順序 ” 比較。

也就是對於兩個字符串 u 、v ,令 i 從 1 開始順次比較 u[i] 和 v[i] ,如果u[i]=v[i] 則令 i 加 1 ,否則若 u[i]<v[i] 則認為 u<v ,u[i]>v[i] 則認為 u>v,比較結束。

如果 i>len(u) 或者 i>len(v) 仍比較不出結果,那么若 len(u)<len(v)則認為 u<v , 若 len(u)=len(v) 則 認 為 u=v ,若 len(u)>len(v) 則 u>v 。

從字符串的大小比較的定義來看, S 的兩個開頭位置不同的后綴 u 和 v 進行比較的結果不可能是相等,因為 u=v 的必要條件 len(u)=len(v)在這里不可能滿足。

后綴數組 SA

后綴數組 SA 是一個一維數組,它保存 1..n 的某個排列 SA[1],SA[2],……,SA[n],並且保證 Suffix(SA[i]) < Suffix(SA[i+1]),1≤i<n。

也就是將 S 的 n 個后綴從小到大進行排序之后把排好序的后綴的開頭位置順次放入 SA 中。

簡單的記憶就是“排第幾的是誰”。

 

名次數組 Rank

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

后綴數組和名次數組為互逆運算。若 sa[i]=j,則 rank[j]=i。

簡單的記憶就是“你排第幾”。

 

字符串aabaaaab的SA數組與Rank數組

 

后綴數組的構造

如何構造后綴數組呢?最直接最簡單的方法當然是把S的后綴都看作一些普通的字符串,按照一般字符串排序的方法對它們從小到大進行排序。

復雜度太高不能滿足我們的需要。

后綴數組有兩種主流的構造方法,倍增算法(double_algorithm)與三分算法(Difference Cover modulo 3)。

倍增算法的思想與ST的思想差不多。將后綴長度依次分為1,2,4,8,。。。,2^k進行排序。進行當前排序時利用到上次的排序結果。

比較suffix(i)和suffix(j)只需先比較紅色部分,再比較綠色部分。

倍增算法正是充分利用了各個后綴之間的聯系,將構造后綴數組的最壞時間復雜度成功降至O(nlogn)。

 

倍增算法的主要思路

用倍增的方法對每個字符開始的長度為 2^k 的子字符串進行排序,求出排名,即 rank 值。

k 從 0 開始,每次加 1,當 2^k 大於 n 以后,每個字符開始的長度為 2^k 的子字符串便相當於所有的后綴。

並且這些子字符串都一定已經比較出大小,即 rank 值中沒有相同的值,那么此時的 rank 值就是最后的結果。

每一次排序都利用上次長度為 2^k-1的字符串的 rank 值,那么長度為 2^k 的字符串就可以用兩個長度為 2^k-1 的字符串的排名作為關鍵字表示,

然后進行基數排序,便得出了長度為 2^k的字符串的 rank 值。以字符串“aabaaaab”為例,整個過程如圖所示。

其中 x、y 是表示長度為 2^k的字符串的兩個關鍵字 。

代碼

 1 int wa[maxn],wb[maxn],wv[maxn],ws[maxn];
 2 int cmp(int *r,int a,int b,int l) {
 3     return r[a]==r[b]&&r[a+l]==r[b+l];
 4 }
 5 void da(int *r,int *sa,int n,int m) {
 6     int i,j,p,*x=wa,*y=wb,*t;
 7     for(i=0; i<m; i++) ws[i]=0;
 8     for(i=0; i<n; i++) ws[x[i]=r[i]]++;
 9     for(i=1; i<m; i++) ws[i]+=ws[i-1];
10     for(i=n-1; i>=0; i--) sa[--ws[x[i]]]=i;
11     for(j=1,p=1; p<n; j*=2,m=p) {
12         for(p=0,i=n-j; i<n; i++) y[p++]=i;
13         for(i=0; i<n; i++) if(sa[i]>=j) y[p++]=sa[i]-j;
14         for(i=0; i<n; i++) wv[i]=x[y[i]];
15         for(i=0; i<m; i++) ws[i]=0;
16         for(i=0; i<n; i++) ws[wv[i]]++;
17         for(i=1; i<m; i++) ws[i]+=ws[i-1];
18         for(i=n-1; i>=0; i--) sa[--ws[wv[i]]]=y[i];
19         for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1; i<n; i++)
20             x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
21     }
22 }
倍增法構造后綴數組

 

代碼講解

待排序的字符串放在 r 數組中,從 r[0]到 r[n-1],長度為 n,且最大值小於 m。為了函數操作的方便,約定除 r[n-1]外所有的 r[i]都大於 0, r[n-1]=0。

函數結束后,結果放在 sa 數組中,從 sa[0]到 sa[n-1]。

函數的第一步,要對長度為 1 的字符串進行排序。

一般來說,在字符串的題目中,r 的最大值不會很大,所以這里使用了基數排序。如果 r 的最大值很大,那么把這段代碼改成快速排序。

代碼:

1 for(i=0;i<m;i++) ws[i]=0;
2 for(i=0;i<n;i++) ws[x[i]=r[i]]++;
3 for(i=1;i<m;i++) ws[i]+=ws[i-1];
4 for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i;

這里 x 數組保存的值相當於是 rank 值。

 

下面的操作只是用 x 數組來比較字符的大小,所以沒有必要求出當前真實的 rank 值。

接下來進行若干次基數排序,在實現的時候,這里有一個小優化。

基數排序要分兩次,第一次是對第二關鍵字排序,第二次是對第一關鍵字排序。

對第二關鍵字排序的結果實際上可以利用上一次求得的 sa 直接算出,沒有必要再算一次。

代碼:

1 for(p=0,i=n-j;i<n;i++) y[p++]=i;
2 for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;

其中變量j是當前字符串的長度,數組y保存的是對第二關鍵字排序的結果 。

 

然后要對第一關鍵字進行排序,代碼:

1 for(i=0;i<n;i++) wv[i]=x[y[i]];
2 for(i=0;i<m;i++) ws[i]=0;
3 for(i=0;i<n;i++) ws[wv[i]]++;
4 for(i=1;i<m;i++) ws[i]+=ws[i-1];
5 for(i=n-1;i>=0;i--) sa[--ws[wv[i]]]=y[i];

這樣便求出了新的 sa 值。在求出 sa 后,下一步是計算 rank 值。

 

這里要注意的是,可能有多個字符串的 rank 值是相同的,所以必須比較兩個字符串是否完全相同, y 數組的值已經沒有必要保存,為了節省空間,這里用 y 數組保存 rank值。

這里又有一個小優化,將 x 和 y 定義為指針類型,復制整個數組的操作可以用交換指針的值代替,不必將數組中值一個一個的復制。

代碼:

1 for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
2 x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;

其中 cmp 函數的代碼是:

1 int cmp(int *r,int a,int b,int l)
2 {return r[a]==r[b]&&r[a+l]==r[b+l];}

這里可以看到規定 r[n-1]=0 的好處,如果 r[a]=r[b],說明以 r[a]或 r[b]開頭的長度為l的字符串肯定不包括字符r[n-1],所以調用變量r[a+l]和r[b+l]不會導致數組下標越界,這樣就不需要做特殊判斷。

 

執行完上面的代碼后,rank值保存在 x 數組中,而變量 p 的結果實際上就是不同的字符串的個數。

這里可以加一個小優化,如果 p 等於 n,那么函數可以結束。

因為在當前長度的字符串中 ,已經沒有相同的字符串,接下來的排序不會改變 rank 值。

例如圖中的第四次排序,實際上是沒有必要的。

對上面的兩段代碼,循環的初始賦值和終止條件可以這樣寫:

1 for(j=1,p=1;p<n;j*=2,m=p) {…………}

在第一次排序以后,rank 數組中的最大值小於 p,所以讓 m=p。

 

整個倍增算法基本寫好,代碼大約 25 行。

算法分析:

倍增算法的時間復雜度比較容易分析。每次基數排序的時間復雜度為 O(n),

排序的次數決定於最長公共子串的長度,最壞情況下,排序次數為 logn 次,所以總的時間復雜度為 O(nlogn)。

 

后綴數組的應用

后綴數組的一些性質

height 數組

定義 height[i]=suffix(sa[i-1])和 suffix(sa[i])的最長公共前綴,也就是排名相鄰的兩個后綴的最長公共前綴。

對於同一個后綴,與他排得越近的后綴的最長公共前綴一定更長。

對於height[]數組的計算,我們並不能順序遞推。需要充分利用字符串之間的聯系,改變他的計算順序。

我們定義h[i]: h[i] = height[rank[i]]

即suffix(i)與排名在它前一位的后綴的最長公共前綴長度。

h[i]有一個重要性質:h[i] >= h[i-1] - 1,所以 suffix(i) 和在它前一名的后綴的最長公共前綴至少是h[i-1]-1。

按照從h[1],h[2],……,h[n]的順序計算h,並利用h數組的性質,復雜度可以降為O(n)。

1 int rank[maxn],height[maxn];
2 void calheight(int *r,int *sa,int n) {
3     int i,j,k=0;
4     for(i=1; i<=n; i++) rank[sa[i]]=i;
5     for(i=0; i<n; height[rank[i++]]=k)
6         for(k?k--:0,j=sa[rank[i]-1]; r[i+k]==r[j+k]; k++);
7     return;
8 }
遞推求height數組

 

LCP 問題

LCP(Longest Common Prefix),定義:LCP(j, k)=后綴 j 與后綴 k 的最長公共前綴長度。

那么對於 j 和 k,不妨設rank[j]<rank[k],則有以下性質:

suffix(j) 和 suffix(k) 的 最 長 公 共 前 綴 為 height[rank[j]+1],height[rank[j]+2], height[rank[j]+3], … ,height[rank[k]]中的最小值。

例如,字符串為“aabaaaab”,求后綴“abaaaab”和后綴“aaab”的最長公共前綴,如圖所示:

因此求兩個后綴的最長公共前綴可以轉化為求某個區間上的最小值。

對於這個 RMQ 問題,可以用 ST 算法對 height 數組做 O(nlogn) 的預處理,用 O(1) 的復雜度進行查詢。

 

算法總流程

1、double_algorithm 構造后綴數組;。。。。。O(nlogn)

2、線性計算出h[]數組,再逐個推出height[i];。。。O(n)

3、ST算法對height[]做預處理;。。。。。。。O(nlogn)

4、查詢LCP(I,J)只需查詢height[rank[i]…rank[j]]中的最小值 O(1)

 

后綴數組的相關問題

 

最長公共前綴 poj 2774

給定一個字符串,詢問某兩個后綴的最長公共前綴。

 

對 height 數組做 RMQ 即可。

 

最長回文子串 ural1297

一個回文串是指滿足如下性質的字符串u:

 

u[i]=u[len(u)-i+1],對所有的1≤i≤len(u)。

也就是說,回文串u是關於u的中間位置“對稱”的。

按照回文串的長度的奇偶性把回文串分為兩類:長度為奇數的回文串稱為奇回文串,長度為偶數的回文串稱為偶回文串。

給定一個字符串,求最長回文子串。

算法分析:

窮舉每一位,然后計算以這個字符為中心的最長回文子串。

注意這里要分兩種情況,一是回文子串的長度為奇數,二是長度為偶數。

兩種情況都可以轉化為求一個后綴和一個反過來寫的后綴的最長公共前綴。

具體的做法是:將整個字符串反過來寫在原字符串后面,中間用一個特殊的字符隔開。

這樣就把問題變為了求這個新的字符串的某兩個后綴的最長公共前綴。

最長公共子串 pku2774

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

算法分析:

字符串的任何一個子串都是這個字符串的某個后綴的前綴。

求 A 和 B 的最長公共子串等價於求 A 的后綴和 B 的后綴的最長公共前綴的最大值。

如果枚舉 A和 B 的所有的后綴,那么這樣做顯然效率低下。

由於要計算 A 的后綴和 B 的后綴的最長公共前綴,所以先將第二個字符串寫在第一個字符串后面,中間用一個沒有出現過的字符隔開,再求這個新的字符串的后綴數組。

觀察一下,看看能不能從這個新的字符串的后綴數組中找到一些規律。以 A=“aaaba”,B=“abaa”為例,如圖所示。

那么是不是所有的 height 值中的最大值就是答案呢?

不一定!有可能這兩個后綴是在同一個字符串中的,所以實際上只有當 suffix(sa[i-1])和suffix(sa[i])不是同一個字符串中的兩個后綴時,height[i]才是滿足條件的。

而這其中的最大值就是答案。記字符串 A 和字符串 B 的長度分別為|A|和|B|。

求新的字符串的后綴數組和 height 數組的時間是 O(|A|+|B|),然后求排名相鄰但原來不在同一個字符串中的兩個后綴的 height 值的最大值,時間也是O(|A|+|B|),

所以整個做法的時間復雜度為 O(|A|+|B|)。時間復雜度已經取到下限,由此看出,這是一個非常優秀的算法。

不相同的子串的個數 spoj694 spoj705

給定一個字符串,求不相同的子串的個數。

算法分析:

每個子串一定是某個后綴的前綴,那么原問題等價於求所有后綴之間的不相同的前綴的個數。

如果所有的后綴按照 suffix(sa[1]), suffix(sa[2]),suffix(sa[3]), …… ,suffix(sa[n])的順序計算,不難發現,對於每一次新加進來的后綴 suffix(sa[k]),它將產生 n-sa[k]+1 個新的前綴。

但是其中有height[k]個是和前面的字符串的前綴是相同的。

所以 suffix(sa[k])將“貢獻”出 n-sa[k]+1- height[k]個不同的子串。累加后便是原問題的答案。

這個做法的時間復雜度為 O(n)。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM