寫在前面
在字符串處理當中,后綴樹和后綴數組都是非常有力的工具。
其中后綴樹大家了解得比較多,關於后綴數組則很少見於國內的資料。
其實后綴數組是后綴樹的一個非常精巧的替代品,它比后綴樹容易編程實現,
能夠實現后綴樹的很多功能而時間復雜度也不太遜色,並且,它比后綴樹所占用的空間小很多。
可以說,在信息學競賽中后綴數組比后綴樹要更為實用!
因此在本文中筆者想介紹一下后綴數組的基本概念、構造方法,
以及配合后綴數組的最長公共前綴數組的構造方法,最后結合一些例子談談后綴數組的應用。
What Is Suffix Array?
學習后綴數組需要認識幾個概念:
子串
字符串S的子串r[i..j],i<=j,表示S串中從i到j這一段,就是順次排列r[i],r[i+1],...,r[j]形成的子串。
后綴
后綴是指從某個位置 i 開始到整個串末尾結束的一個特殊子串。字符串r的從第i個字符開始的后綴表示為Suffix(i),
也就是Suffix(i)=S[i...len(S)-1] 。
后綴數組(SA[i]存放排名第i大的后綴首字符下標)
后綴數組 SA 是一個一維數組,它保存1..n 的某個排列SA[1] ,SA[2] ,...,SA[n] ,
並且保證Suffix(SA[i])<Suffix(SA[i+1]), 1<=i<n 。
也就是將S的n個后綴從小到大進行排序之后把排好序的后綴的開頭位置順次放入SA 中。
名次數組(rank[i]存放suffix(i)的優先級)
名次數組 Rank[i] 保存的是 Suffix(i) 在所有后綴中從小到大排列的“名次”
注:這個是排序的關鍵字~(這句話是我們排序的重點)


(我的理解):
sa[i]:保存的是S字符串的所有后綴在以字典序排序后,排在第i名的字符串在原來子串中的位置。
rank[i]:保存的是S字符串的所有后綴在以字典序排序后,原來的第i名現在排第幾。
簡單的說,后綴數組(SA)是“排第幾的是誰?”,名次數組(RANK)是“你排第幾?”。
容易看出,后綴數組和名次數組為互逆運算。我們只要算出了sa數組,就可以在O(n)的時間復雜度內算出rank數組。
height數組:height[i]保存的是suffix(i)和suffix(i-1)的最長公共前綴的長度。也就是排名相鄰的兩個后綴的最長公共前綴。
How To Build Suffix Array?
要構造Suffix Array,主要就是構造sa數組,rank數組和height數組。
首先來看一下如何構造sa數組:
構造sa數組的方法有三種:
1)倍增算法:O(nlongn)
2)DC3算法:O(n)
3)skew算法(不常用)
這里主要講一下DC3算法:
DC3算法是一個優秀的線性算法!
很多人都認為DC3算法很復雜,其實也沒多復雜,代碼也就40多行,只是for循環多了點。
DC3算法:
1) 先將后綴分成兩部分,然后對第一部分的后綴排序。
字符的編號從0開始。
將后綴分成兩部分:
第一部分是后綴k(k模3不等於0)
第二部分是后綴k(k模3等於0)
2) 利用(1)的結果,對第二部分的后綴排序。
3) 將(1)和(2)的結果合並,即完成對所有后綴排序。
於是求出了所有后綴的排序,有什么用呢?主要是用於求它們之間的最長公共前綴(Longest Common Prefix,LCP)。
求出sa數組之后,根據rank[sa[i]]=i,rank數組自然也就能夠在O(n)的時間內求出。
那我們如何快速的求出height數組呢?
令LCP(i,j)為第i小的后綴和第j小的后綴(也就是Suffix(SA[i])和Suffix(SA[j]))的最長公共前綴的長度,則有如下兩個性質:
-
對任意i<=k<=j,有LCP(i,j) = min(LCP(i,k),LCP(k,j))
-
LCP(i,j)=min(i<k<=j)(LCP(k-1,k))
令height[i]=LCP(i-1,i),即height[i]代表第i小的后綴與第i-1小的后綴的LCP,則求LCP(i,j)就等於求height[i+1]~height[j]之間的RMQ,套用RMQ算法就可以了,復雜度是預處理O(nlogn),查詢O(1).
這樣一來我們就將height數組也求出來了。
下面用草稿紙來模擬一遍:
例如:
aabaaaab
總共有n=8個后綴:
1: aabaaaab
2: abaaaab
3: baaaab
4: aaaab
5: aaab
6: aab
7: ab
8: b
按照字典序排序后
sa[ 1 ] = 4 aaaab
sa[ 2 ] = 5 aaab
sa[ 3 ] = 6 aab
sa[ 4 ] = 1 aabaaaab
sa[ 5 ] = 7 ab
sa[ 6 ] = 2 abaaaab
sa[ 7 ] = 8 b
sa[ 8 ] = 3 baaaab
rank數組為:
rank[1]=4
rank[2]=6
rank[3]=8
rank[4]=1
rank[5]=2
rank[6]=3
rank[7]=5
rank[8]=7
height數組為:
height[ 1 ]=null
height[ 2 ]= 3
height[ 3 ]= 2
height[ 4 ]= 3
height[ 5 ]= 1
height[ 6 ]= 2
height[ 7 ]= 0
height[ 8 ]= 1
因此,所有子串的最長公共子串就是3.
這里給出一個理解程序:
/* * this code is made by crazyacking * Verdict: Accepted * Submission Date: 2015-05-09-21.22 * Time: 0MS * Memory: 137KB */ #include <queue> #include <cstdio> #include <set> #include <string> #include <stack> #include <cmath> #include <climits> #include <map> #include <cstdlib> #include <iostream> #include <vector> #include <algorithm> #include <cstring> #define LL long long #define ULL unsigned long long using namespace std; const int MAXN=100010; //以下為倍增算法求后綴數組 int wa[MAXN],wb[MAXN],wv[MAXN],Ws[MAXN]; int cmp(int *r,int a,int b,int l) {return r[a]==r[b]&&r[a+l]==r[b+l];} /**< 傳入參數:str,sa,len+1,ASCII_MAX+1 */ void da(const char *r,int *sa,int n,int m) { int i,j,p,*x=wa,*y=wb,*t; for(i=0; i<m; i++) Ws[i]=0; for(i=0; i<n; i++) Ws[x[i]=r[i]]++; for(i=1; i<m; i++) Ws[i]+=Ws[i-1]; for(i=n-1; i>=0; i--) sa[--Ws[x[i]]]=i; for(j=1,p=1; p<n; j*=2,m=p) { for(p=0,i=n-j; i<n; i++) y[p++]=i; for(i=0; i<n; i++) if(sa[i]>=j) y[p++]=sa[i]-j; for(i=0; i<n; i++) wv[i]=x[y[i]]; for(i=0; i<m; i++) Ws[i]=0; for(i=0; i<n; i++) Ws[wv[i]]++; for(i=1; i<m; i++) Ws[i]+=Ws[i-1]; for(i=n-1; i>=0; i--) sa[--Ws[wv[i]]]=y[i]; for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1; i<n; i++) x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++; } return; } int sa[MAXN],Rank[MAXN],height[MAXN]; //求height數組 /**< str,sa,len */ void calheight(const char *r,int *sa,int n) { int i,j,k=0; for(i=1; i<=n; i++) Rank[sa[i]]=i; for(i=0; i<n; height[Rank[i++]]=k) for(k?k--:0,j=sa[Rank[i]-1]; r[i+k]==r[j+k]; k++); // Unified for(int i=n;i>=1;--i) ++sa[i],Rank[i]=Rank[i-1]; } char str[MAXN]; int main() { while(scanf("%s",str)!=EOF) { int len=strlen(str); da(str,sa,len+1,130); calheight(str,sa,len); puts("--------------All Suffix--------------"); for(int i=1; i<=len; ++i) { printf("%d:\t",i); for(int j=i-1; j<len; ++j) printf("%c",str[j]); puts(""); } puts(""); puts("-------------After sort---------------"); for(int i=1; i<=len; ++i) { printf("sa[%2d ] = %2d\t",i,sa[i]); for(int j=sa[i]-1; j<len; ++j) printf("%c",str[j]); puts(""); } puts(""); puts("---------------Height-----------------"); for(int i=1; i<=len; ++i) printf("height[%2d ]=%2d \n",i,height[i]); puts(""); puts("----------------Rank------------------"); for(int i=1; i<=len; ++i) printf("Rank[%2d ] = %2d\n",i,Rank[i]); puts("------------------END-----------------"); } return 0; }
The Use Of Suffix Array
這里只是簡單的介紹幾種后綴數組的運用,真正的熟練后綴數組,還需要通過不斷的做題、不斷的實踐來掌握。
-
最長公共子串
我們知道,字符串的任何一個子串都可以看作是這個字符串某個的后綴的前綴。
求A和B的最長公共子串等價於求A的后綴和B的后綴的最長公共前綴的最大值。
將第二個字符串寫在第一個字符串的后面,中間用一個沒有出現過的字符隔開,在求出這個新字符串的后綴數組,然后我們只需要找最大的height[i]就可(前提是要判斷是否不在同一個字符串中)。 -
單個字符串的相關問題
-
兩個字符串的相關問題
-
多個字符串的相關問題
