尾碼數組號稱字串處理神器,不過發現好多人都只會用模板,其實這不是我們學演算法的本質,我們學習演算法的本質應該理解其實現原理,並加以實現,特別是演算法,更講究的是一種思想。一年前的我也是只會用別人的模板,最近卻靜下心來,研究了一下尾碼數組,自己寫了一份自己的模板。
我基本上是跟著連教的ppt來學習的,當然也少不了百度,先講一下基本概念。(這裡大量引用了連教的ppt)
基本定義:
子串 注:串!=字串
字串 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)] 。
尾碼數組(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數組和rank數組
易知sa和rank互為逆操作,即sa[rank[i]] = i;
Rank[sa[i]] = i;(所以我們只要求得其一,就能O(n)算出另一個)
註:這個結論只在最後完成排序的時候符合。
但sa和rank的定義一直都是適用的。
原因是最後的時候不會存在相同(rank相等)的兩個子串。
演算法基本流程
•設排序的的當前長度是h。Suffix(i,h) 表示suffix(i)前h個字元(大於length會截斷)•先按H=1,對suffix(i,H)(0<i<s.length)排序•倍增長度H,利用之前排序H/2長度後得到的rank數組作為關鍵字,把後H/2部分作為第二關鍵字,把前H/2部分作為第一關鍵字,對H長度的子串作排序.•由於是倍增長度,所以最多作logn次排序那麼複雜要做到nlogn,顯然排序要o(n),O(n)一般都選計數排序。計數排序:http://baike.baidu.com/view/1209480.htm 不會的自己看看這個,是代碼的主要部分。這裡選計數排序還有一個重要的原因,它是一個穩定排序,這就保證了數組的下標識第二關鍵字,我們前面說了,對於倍增長度H,利用之前排序H/2長度後得到的rank數組作為關鍵字,把後H/2部分作為第二關鍵字,嗯,就是這裡,所以我們要先排後H/2的序,然後得到新的數組序列,下標就是第二關鍵字了,數組裡面就是前H/2
rank的值,這是第一關鍵字,那麼直接排序就相當於先對前H/2排序,如果這裡相等,那麼就會按下標排序,既第二關鍵字排序
以下內容請按代碼手動類比一個串abab的構造過程,求sa數組具體見代碼實現:
•按H=1進行計數排序
//cnt是計數排序的輔助數組,k是第一關鍵字,id是第二關鍵字下標數組,r是以下標為第二關鍵字的新構數組,w存放的是字串資訊。sa儲存的是排第i的是誰int *k = rk,*id = height,*r = res, *cnt = wa;//計數排序rep(i,up) cnt[i] = 0;rep(i,len) cnt[k[i] = w[i]]++;rep(i,up) cnt[i+1] += cnt[i];for(int i = len - 1; i >= 0; i--) {sa[--cnt[k[i]]] = i;}
求第二關鍵字(想想為什麼構造w數組的時候末尾要加個0)
//cnt是計數排序的輔助數組,k是第一關鍵字,id是第二關鍵字下標數組,r是以下標為第二關鍵字的新構數組,w存放的是字串資訊,sa儲存的是排第i的是誰for(int i = len - d; i < len; i++) id[p++] = i;rep(i,len) if(sa[i] >= d) id[p++] = sa[i] - d;//id儲存了按後h/2排序的的序列,即排第i的後h/2的是原數組中的那個rep(i,len) r[i] = k[id[i]];//構造新的排序數組
對新數組排序
//cnt是計數排序的輔助數組,k是第一關鍵字,id是第二關鍵字下標數組,r是以下標為第二關鍵字的新構數組,w存放的是字串資訊,sa儲存的是排第i的是誰rep(i,up) cnt[i] = 0;rep(i,len) cnt[r[i]]++;rep(i,up) cnt[i+1] += cnt[i];for(int i = len - 1; i >= 0; i--) {sa[--cnt[r[i]]] = id[i];}
得到新的關鍵字(即按H長度排序後的離散序列)
//cnt是計數排序的輔助數組,k是第一關鍵字,id是第二關鍵字下標數組,r是以下標為第二關鍵字的新構數組,w存放的是字串資訊,sa儲存的是排第i的是誰swap(k,r);p = 0;k[sa[0]] = p++;rep(i,len-1) {if(sa[i]+d < len && sa[i+1]+d <len &&r[sa[i]] == r[sa[i+1]]&& r[sa[i]+d] == r[sa[i+1]+d])k[sa[i+1]] = p - 1;else k[sa[i+1]] = p++;}
重複以上最後可以得到sa數組Sa和rank有什麼用?求height數組!!
height[i] 表示sa[i]和sa[i-1]的最長首碼,height的構造看代碼手推一定能弄懂,自己看看吧
下面給出全模板代碼
#define rep(i,n) for(int i = 0;i < n; i++)using namespace std;const int size = 200005,INF = 1<<30;int rk[size],sa[size],height[size],w[size],wa[size],res[size];void getSa (int len,int up) {int *k = rk,*id = height,*r = res, *cnt = wa;rep(i,up) cnt[i] = 0;rep(i,len) cnt[k[i] = w[i]]++;rep(i,up) cnt[i+1] += cnt[i];for(int i = len - 1; i >= 0; i--) {sa[--cnt[k[i]]] = i;}int d = 1,p = 0;while(p < len){for(int i = len - d; i < len; i++) id[p++] = i;rep(i,len)if(sa[i] >= d) id[p++] = sa[i] - d;rep(i,len) r[i] = k[id[i]];rep(i,up) cnt[i] = 0;rep(i,len) cnt[r[i]]++;rep(i,up) cnt[i+1] += cnt[i];for(int i = len - 1; i >= 0; i--) {sa[--cnt[r[i]]] = id[i];} swap(k,r);p = 0;k[sa[0]] = p++;rep(i,len-1) {if(sa[i]+d < len && sa[i+1]+d <len &&r[sa[i]] == r[sa[i+1]]&& r[sa[i]+d] == r[sa[i+1]+d])k[sa[i+1]] = p - 1;else k[sa[i+1]] = p++;}if(p >= len) return ;d *= 2,up = p, p = 0;}}void getHeight(int len) {rep(i,len) rk[sa[i]] = i;height[0] = 0;for(int i = 0,p = 0; i < len - 1; i++) {int j = sa[rk[i]-1];while(i+p < len&& j+p < len&& w[i+p] == w[j+p]) {p++;}height[rk[i]] = p;p = max(0,p - 1);}}int getSuffix(char s[]) {int len = strlen(s),up = 0;for(int i = 0; i < len; i++) {w[i] = s[i];up = max(up,w[i]);}w[len++] = 0;getSa(len,up+1);getHeight(len);return len;}
最後給出幾題習題•Poj 2774 –最長公用連續子串,入門題目•Poj1743—最長不重疊重複子串•Hint:二分的判定要小心點,這題有點特別。•Poj3294—出現次數超過一半的最長子串•Hint:判斷組中不同串出現次數的技巧很關鍵•Poj3261—重複k次可重疊子串。•Hint:會了上面兩題,這題應該很簡單,可以試試用單調棧。•Poj2758—尾碼數組+rmq•Hint:這題難度不在rmq,而在於寫代碼的能力和查詢的演算法實現。