后缀数组简要总结
眾所周知,后綴數組是解決字符串類問題的強力工具,一切與字符串公共子串相關聯的都可能與它有關。
方便起見,我們令
?
a為需要處理的子串(長度為len)(從a[1]開始到a[len])
rank[i]數組表示以i位置開始的后綴子串在所有后綴子串的字典序排名(以i位置開始的后綴子串即a[i]~a[len])
sa[i]數組表示排名為i的后綴子串的開始位置
?
根據定義很顯然有 sa[rank[i]]=i,rank[sa[i]]=i
要先牢牢把握好這些數組的含義。
?
?
(來自網絡的一張圖)
后綴數組指的就是sa[i]數組啦,我們現在要想辦法如何求sa[i]數組。
思考了下感覺直接求sa[i]數組非常復雜,因為排名什么的是要建立在求出所有后綴子串的前提下才求得出來。
注意到sa[rank[i]]=i,我們可以先求rank[i]數組,然后就可以知道sa[i]數組了。
很顯然有一個O(len2log len)的算法,就是直接把len個長度為O(len)的字符串排序,但要是我們采用這個算法的話就沒有下文了。
計算機芯片有一個特性,通用的芯片處理能力不高,但它可以處理很多信息,而專用的芯片處理能力就很強,但它只能處理單一方面的信息。
而上述算法正因為是通用,所以效率才不高。我們要提高效率,就要去研究所處理的數據的性質,根據性質我們才能夠得到更高效的算法,而這算法因為運用了這個數據的性質所以通用性減少了,只適用于擁有這一性質的數據。
這里,我們就要靈活運用所有字符串都是a的后綴這一性質(比如所有后綴子串右端點固定),去得到更高效的算法。
這里是一個由Manber和Myers發明的算法,基本思想是倍增。
我們考慮兩個后綴子串i,j(i,j為該后綴子串的首字母位置,且i<j)
我們現在要比較它們的字典序大小,很顯然i子串中,與首字母距離大于j子串長度的字符對大小比較沒有任何貢獻(除了前面全部相等的情況),我們可以把它們忽略。
現在我們要比較的i,j子串的長度相等了,如何去比較呢?
逐位比較的話與上面那個算法沒什么區別,我們將長度折半,將i,j分為前后的1,2部分,我們可以先比較i,j的1部分的大小,1部分大小相同再比較2部分的大小,而比較1部分的大小就又回到了原來比較兩個字符串的大小的子問題了。
由此我們首先計算每個位置開始長度為1的子串的排名,再利用這個結果去計算長度為2的子串的排名,接下來計算長度為4的子串的排名,不斷倍增,直到長度大于等于len,就得到了rank數組了。
當我們計算2k長度的子串排名時,2k-1長度的排名我們已經算出來了。
那么我們只要比較以i位置開始的2k-1長度的子串的排名與以j位置開始的2k-1長度的子串的排名(a[i......i+2k-1]與a[j......j+2k-1]),如果相等則再比較以i+2k-1位置開始的2k-1長度的子串的排名與以j+2k-1位置開始的2k-1長度的子串的排名(a[i+2k-1......i+2k-1+2k-1]與a[j+2k-1......j+2k-1+2k-1]),綜合起來我們就能得到以i位置開始的2k的長度的子串與以j位置開始的2k的長度的子串的相對關系。
我們注意到這里i,j子串的大小比較其實就是比較一個由兩部分排名所構成的兩位數大小的關系,前半部分充當十位數,后半部分充當個位數
那么我們可以根據之前算好的每一位開始,長度為2k-1的子串的排名,去算出每一位開始在2k長度下的所謂的這個兩位數,再把這些數進行離散,就能得到每一位開始長度為2k的子串的排名了。
(來自網絡的一張圖)
接下來就是如何高效地去離散。
如果我們采用快排的話總復雜度就是O((len+len*log len)log len)也就是O(len*log2len),我們還可以再優化成O(len*log len)
我們注意到我們要離散的數只是兩位數,那么我們可以采用基數排序的方法
(一張來自網絡上基數排序的動態圖)
它的時間復雜度是O(n+d),d是數的位數,n是數的個數。在這里復雜度就是O(len+2),而算法的總復雜度就是O((len+len+2)*log len),也就是O(len*log len)
因為每一個后綴的長度都不是一樣的,最終的排名不可能有相同的,那么當離散后最大的數等于a的長度len的話就可以停止了。
這里離散的原理是這樣的,但具體的代碼實現由于過于簡潔而可能會有點讓人感到費解,必要時建議用紙筆畫筆畫。
到這里其實sa[i]數組的求解方法講完的了(然后有道模板題)
但實際上后綴數組還有另外兩個重要的數組,它們才是讓后綴數組成為重復字符串處理強有力的原因。
?
heigh[i]數組表示排名為i的后綴子串與排名為i-1的后綴子串的最長公共前綴長度
h[i]數組表示從i位置開始的后綴子串與排名比它前一名的后綴子串的最長公眾前綴長度,即h[i]=heigh[rank[i]]
?
排名不相鄰的兩個后綴子串的最長公共前綴子串的長度是兩個排名之間heigh的最小值。(看圖很顯然的吧)
(還是來自網上的一張圖)
那我們該如何求解heigh[i]數組呢?
如果我們暴力在sa數組求解的話復雜度就變成O(len2)了(我要你有何用)
注意到h[i]數組有個非常神奇的性質,運用這一性質我們可以把求heigh[i]數組的復雜度降到O(n)
h[i]>=h[i-1]-1
我們假設k=sa[rank[i]-1],即k是 排名比 從i位置開始的后綴子串的排名 前一位的 后綴子串的 開始位置
它們的最長公共前綴長度是h[i],即從i位置開始的后綴子串(a[i.....])與從k位置開始的后綴子串(a[k.....])的前h[i]個字符是相等的
那么,從i+1位置開始的后綴子串(a[i+1.....])與從k+1位置開始的后綴子串(a[k+1.....])分別是兩者去掉首個字符的結果,所以它們頭部的h[i]-1個字符是相等的
雖然在后綴數組中,排名比 從i+1位置開始的后綴子串(a[i+1.....]) 前一位的未必就是 從k+1位置開始的后綴子串(a[k+1.....])(即sa[rank[i+1]-1]的值不一定是k+1),但即便如此,公共前綴的長度也是會只增不減,所以這也是為什么是h[i+1]>=h[i]-1的原因。
因此,當計算i+1的h[i+1]時,只要從h[i]-1的長度開始檢查,計算最長公共前綴的長度就好了。由于h最多只會增加len次,所以總的復雜度就是O(len)了。
得到h[i]數組后,heigh[i]數組也就能根據h[i]=heigh[rank[i]]算出來了。
這里的計算雖然簡單,但非常巧妙,使用了類似尺取法的技巧。
當我們擁有heigh[i]數組后,我們就可以非常方便地求解很多問題了。
結合RMQ里的ST算法后,我們可以O(1)的時間得到任意兩個后綴的最長公共前綴了。
1 #include <algorithm> 2 #include <cstring> 3 #include <cstdlib> 4 #include <cstdio> 5 #include <iostream> 6 #include <cmath> 7 #include <ctime> 8 #include <queue> 9 #define MIN(a,b) (((a)<(b)?(a):(b))) 10 #define MAX(a,b) (((a)>(b)?(a):(b))) 11 #define ABS(a) (((a)>0?(a):-(a))) 12 #define debug(a) printf("a=%d\n",a); 13 #define fo(i,a,b) for (int i=(a);i<=(b);++i) 14 #define fod(i,a,b) for (int i=(a);i>=(b);--i) 15 #define rep(i,a,b) for (int i=(a);i<(b);++i) 16 #define red(i,a,b) for (int i=(a);i>(b);--i) 17 #define N 200200 18 typedef long long LL; 19 using namespace std; 20 int heigh[N],sa[N],rank[N],tp[N],cnt[N],len; //tp[i]是基數排序的輔助數組,意義與sa[i]一樣,cnt[i]即桶 21 char a[N]; 22 void readint(int &x){ 23 x=0; 24 char c; 25 int w=1; 26 for (c=getchar();c<'0'||c>'9';c=getchar()) 27 if (c=='-') w=-1; 28 for (;c>='0'&&c<='9';c=getchar()) 29 x=(x<<3)+(x<<1)+c-'0'; 30 x*=w; 31 } 32 void readlong(long long &x){ 33 x=0; 34 char c; 35 long long w=1; 36 for (c=getchar();c<'0'||c>'9';c=getchar()) 37 if (c=='-') w=-1; 38 for (;c>='0'&&c<='9';c=getchar()) 39 x=(x<<3)+(x<<1)+c-'0'; 40 x*=w; 41 } 42 void init(){ 43 scanf("%s",a+1); 44 len=strlen(a+1); 45 } 46 void rsort(){ //rank[i]數組相當于十位數(前部分),tp[i]數組相當于個位數(后部分),注意tp[i]與rank[i]意義不一樣! 47 fo(i,1,m) cnt[i]=0; 48 fo(i,1,len) ++cnt[rank[tp[i]]]; 49 fo(i,2,m) cnt[i]+=cnt[i-1]; //前綴和 50 fod(i,len,1) sa[cnt[rank[tp[i]]]--]=tp[i]; //個位(后部分子串排名)排后的十位(前部分子串排名)的排名 51 } 52 bool check(int *f,int x,int y,int w){ 53 return (f[x]==f[y]&&f[x+w]==f[y+w]); 54 } 55 void get_sa(int m){ 56 fo(i,1,len) {rank[i]=a[i]; tp[i]=i;} //初始化 57 rsort(); 58 for (int w=1,p=0;p<len;w<<=1,m=p){ //w為當前子串長度,p為離散后的最大數 59 p=0; 60 fo(i,len-w+1,len) tp[++p]=i; //此部分子串沒有后部分 61 fo(i,1,len) if (sa[i]>w) tp[++p]=sa[i]-w; //根據sa[i]數組得到后部分(個位)排名靠前的,并把其前部分的開頭位置儲存在tp[i]數組上 62 rsort(); 63 swap(rank,tp); //交換兩數組,因為離散需用到原rank數組,此處為節省空間而交換 64 rank[sa[1]]=p=1; 65 fo(i,2,len) rank[sa[i]]=check(tp,sa[i],sa[i-1],w)?p:++p; //若兩子串前后兩部分的排名分別一樣,則視為同一子串,排名一樣 66 } 67 for (int i=1,k=0,j=0;i<=len;heigh[rank[i++]]=k) //heigh[rank[i]]=h[i] 68 for(k=k?k-1:k,j=sa[rank[i]-1];a[i+k]==a[j+k];++k); //逐位比較 69 } 70 int main(){ 71 freopen("testdata.in", "r", stdin); 72 freopen("test.out", "w", stdout); 73 init(); 74 get_sa(128); 75 return 0; 76 } 后綴數組注意tp[i]數組與rank[i]數組意義不一樣!!rank[i]表示從位置i開始的后綴子串的排名,tp[i]表示排名第i的后綴子串的起始位置!!!
?
以下是一些應用的求法。
1.求字符串中可重疊的最長公共子串
根據height定義很顯然這個就是height中的最大值了。
2.求字符串中不可重疊的最長公共子串
這個我們需要二分答案再驗證,我們要二分可能的公共子串長度k,然后按sa的順序對height進行分組,使組內的height值都不小于k,然后對于某個組內我們只要考察該組內sa的最大值和最小值的差是否大于等于k(實際上就是這兩個后綴的開頭是否相差k,從而避免重疊),有則k成立。
3.求字符串中可重疊K次的最長公共子串
這個我們跟2差不多,二分公共長度k,進行分組,然后我們考察每個組內的后綴個數是否大于等于K,有則K成立。
4.求字符串中不相同的子串個數
每個子串必定是某個后綴的前綴,那問題就是求所有后綴中不相同的前綴的個數,我們從順序sa[1],sa[2],sa[3],不難發現每加入一個從位置i開始的后綴子串,它有n-sa[i]+1個前綴(就是這個后綴的長度),其中有height[i]是和前面的字符串相同(最長公共前綴嘛),所以這個字符串會貢獻出n-sa[i]+1-height[i]不同的子串,累加后就可以了。
5.求字符串中最長的回文子串
所謂回文就是一個字符串滿足中心對稱,某個字符為對稱中心,從這個字符向左和向右對應位置的字母都相等,如12345678987654321,我們設這個中心對稱的字符為a[i],則我們就要判斷 a[i-k]與a[i+k]是否相等,我們可以把整個字符串倒過來寫在這個字符串后面(我們就得到了逆過來的那個12345678的字符串),其中加個特殊符號,這樣我們可以簡化判斷,只用判斷這新的字符串的某兩個字符串的最長公共前綴(關于回文的查找還有Manacher算法等更為高效簡單的算法)
6.求字符串中連續的重復子串
已知一個字符串L是由某個字符串S重復R次得到的,求重復次數R的最大值。
我們假設S的長度為k,首先L%k=0,然后判斷從1位置開始的后綴子串和從k+1位置開始的后綴子串的最長公共前綴子串是否為n-k。因此在查找最長公共子串的時候就是求height[rank[k+1]]到height[rank[1]]之間的最小值。我們的做法就是求height數組中每一個數到height[rank[1]]之間的數的最小值k,R=L的長度/K
7.求字符串中重復次數最多的連續重復子串
8.求兩個字符串的最長公共子串
將這兩個字符串連接起來,其中用一個特殊符號分開,然后再求出不在同一個字符串中的最大的height值即可。
9.求兩個字符串長度不小于K的最長公共子串
將兩個字符串A、B連接起來,其中用一個特殊符號分開,然后用k對height數組分組,再統計每組的最長公共前綴和。每遇到一個B子串,就統計與前面A子串產生多少個長度不小于K的公共子串,這里A需要用棧來維護。然后對A也一樣的處理。
10.求n個字符串的最長公共子串
這個用KMP可以處理,也可以將這n個字符串連成一個字符串,然后用不同的特殊符號分開,二分長度k對height數組進行分組判定是否該組中所有字符串的子串都出現在里面即可。
?
POJ 2774 求兩個子串的最長公共子串長度
1 #include <algorithm> 2 #include <cstring> 3 #include <cstdlib> 4 #include <cstdio> 5 #include <iostream> 6 #include <cmath> 7 #include <ctime> 8 #include <queue> 9 #define MIN(a,b) (((a)<(b)?(a):(b))) 10 #define MAX(a,b) (((a)>(b)?(a):(b))) 11 #define ABS(a) (((a)>0?(a):-(a))) 12 #define debug(a) printf("a=%d\n",a); 13 #define fo(i,a,b) for (int i=(a);i<=(b);++i) 14 #define fod(i,a,b) for (int i=(a);i>=(b);--i) 15 #define rep(i,a,b) for (int i=(a);i<(b);++i) 16 #define red(i,a,b) for (int i=(a);i>(b);--i) 17 #define N 200200 18 typedef long long LL; 19 using namespace std; 20 int heigh[N],sa[N],rank[N],tp[N],cnt[N],la,lb,ans,m; 21 char a[N]; 22 void readint(int &x){ 23 x=0; 24 char c; 25 int w=1; 26 for (c=getchar();c<'0'||c>'9';c=getchar()) 27 if (c=='-') w=-1; 28 for (;c>='0'&&c<='9';c=getchar()) 29 x=(x<<3)+(x<<1)+c-'0'; 30 x*=w; 31 } 32 void readlong(long long &x){ 33 x=0; 34 char c; 35 long long w=1; 36 for (c=getchar();c<'0'||c>'9';c=getchar()) 37 if (c=='-') w=-1; 38 for (;c>='0'&&c<='9';c=getchar()) 39 x=(x<<3)+(x<<1)+c-'0'; 40 x*=w; 41 } 42 void init(){ 43 scanf("%s",a+1); 44 la=strlen(a+1); 45 scanf("%s",a+1+la); 46 lb=strlen(a+1); 47 } 48 void rsort(){ 49 fo(i,1,m) cnt[i]=0; 50 fo(i,1,lb) ++cnt[rank[tp[i]]]; 51 fo(i,2,m) cnt[i]+=cnt[i-1]; 52 fod(i,lb,1) sa[cnt[rank[tp[i]]]--]=tp[i]; 53 } 54 bool check(int *f,int x,int y,int w){ 55 return (f[x]==f[y]&&f[x+w]==f[y+w]); 56 } 57 void get_sa(){ 58 m=128; 59 fo(i,1,lb) {rank[i]=a[i]; tp[i]=i;} 60 rsort(); 61 for (int w=1,p=0;p<lb;w<<=1,m=p){ 62 p=0; 63 fo(i,lb-w+1,lb) tp[++p]=i; 64 fo(i,1,lb) if (sa[i]>w) tp[++p]=sa[i]-w; 65 rsort(); 66 swap(rank,tp); 67 rank[sa[1]]=p=1; 68 fo(i,2,lb) rank[sa[i]]=check(tp,sa[i],sa[i-1],w)?p:++p; 69 } 70 for (int i=1,k=0,j=0;i<=lb;heigh[rank[i++]]=k) 71 for(k=k?k-1:k,j=sa[rank[i]-1];a[i+k]==a[j+k];++k); 72 } 73 void solve(){ 74 ans=0; 75 fo(i,2,lb) if ((sa[i]<=la&&sa[i-1]>la)||(sa[i]>la&&sa[i-1]<=la)) ans=MAX(ans,heigh[i]); 76 } 77 int main(){ 78 init(); 79 get_sa(); 80 solve(); 81 printf("%d\n",ans); 82 return 0; 83 } 神奇的代碼轉載于:https://www.cnblogs.com/Lanly/p/11414507.html
總結
- 上一篇: Python访问MySQL
- 下一篇: jQuery .attr() vs .p