#前言 一道模板題 後綴數組(SA)是一個比較強大的處理字符串的算法,是有關字符串的比較基礎是嗎?算法,因此必須掌握 實現主要有倍增和$DC3$,而我太弱了只學了倍增 #目錄 ##知識點 ###1.基數排序+倍增 ###2.最長公共前綴Height ##一些要維護的東西 $s$:就是這個字符串,長度爲$len$ $rank[i]$:表示$i到len$這個後綴在全部後綴中的排名 $sa[i]$:表示排名爲$i$的後綴的首字符下標 $height[i]$:表示相鄰兩個排名的後綴的的最長公共前綴的長度 #基數排序+倍增構造SA 首先基數排序:就是低位到高位一位一位桶排,詳見baidu 怎麼排序? 若是一位一位來,那不就成了暴力了 因此咱們要倍增排序 具體來講,對於一個長度爲$2^k$的字符串,咱們能夠把它當作是兩個長度爲$2^{k-1}$的字符串組成的,那麼就至關於它有兩個關鍵字,咱們就從長度爲$1$開始,每次對這兩個關鍵字排序不就好了 一張古老的圖 看代碼c++
# include <bits/stdc++.h> # define IL inline # define RG register # define Fill(a, b) memset(a, b, sizeof(a)) using namespace std; typedef long long ll; const int _(1e6 + 5); int len, sa[_], t[_], a[_], rk[_], y[_]; char s[_]; //t是桶;a和s同樣;rk就是rank;y是輔助rk的數組,和sa性質類似;sa就是sa IL bool Cmp(RG int i, RG int j, RG int k){ return y[i] == y[j] && y[i + k] == y[j + k]; } //肯定兩個子串是否相同 IL void Sort(){ RG int m = 80; //初始字符集大小 for(RG int i = 1; i <= len; ++i) ++t[rk[i] = a[i]]; for(RG int i = 1; i <= m; ++i) t[i] += t[i - 1]; for(RG int i = len; i; --i) sa[t[rk[i]]--] = i; //初始一個字符的排序 for(RG int k = 1; k <= len; k <<= 1){ //倍增 RG int l = 0; for(RG int i = 0; i <= m; ++i) y[i] = 0; //先按第二關鍵字排序,y記下編號 for(RG int i = len - k + 1; i <= len; ++i) y[++l] = i; //越界的第二關鍵字沒有,放前面 for(RG int i = 1; i <= len; ++i) if(sa[i] > k) y[++l] = sa[i] - k; //剩下的按順序排 //再按rk第一關鍵字排序 for(RG int i = 0; i <= m; ++i) t[i] = 0; for(RG int i = 1; i <= len; ++i) ++t[rk[y[i]]]; for(RG int i = 1; i <= m; ++i) t[i] += t[i - 1]; for(RG int i = len; i; --i) sa[t[rk[y[i]]]--] = y[i]; //用此次的rk更新下次的 swap(rk, y); rk[sa[1]] = l = 1; for(RG int i = 2; i <= len; ++i) rk[sa[i]] = Cmp(sa[i], sa[i - 1], k) ? l : ++l; //相同的rk同樣 if(l >= len) break; //此時確定不用排了,你們都不一樣 m = l; //記錄下次排序的字符集大小 } } int main(RG int argc, RG char* argv[]){ scanf(" %s", s + 1); len = strlen(s + 1); for(RG int i = 1; i <= len; ++i) a[i] = s[i] - '0'; Sort(); for(RG int i = 1; i <= len; ++i) printf("%d ", sa[i]); return puts(""), 0; }
有點難理解,多看幾個博客就能夠了 #最長公共前綴---Height 有個$sa$和$rank$並無什麼卵用,這個時候就有$Height$這個美妙的東西 怎麼求? 暴力求$O(n^2)$顯然不行 那麼這個時候要利用$h$的美妙性質:$h[i]≥h[i-1]-1$ 證實:設後綴$suf[k]和suf[i-1]$爲兩個相鄰排名的後綴,它們的最長公共前綴就是$h[i-1]$ 同時去掉第一個字符,那麼就是$suf[k+1]和suf[i]$,那它們兩個的最長公共前綴顯然就是$h[i-1]-1$ 因此$suf[i]$和排在它前面的後綴的最長公共前綴至少是$h[i-1]-1$算法
那麼$h$就能夠很快求出來了,那麼$height$也就能很快求出來了數組
for(RG int i = 1; i <= len; ++i){ h[i] = max(0, h[i - 1] - 1); if(rk[i] == 1) continue; while(a[i + h[i]] == a[sa[rk[i] - 1] + h[i]]) ++h[i]; } for(RG int i = 1; i <= len; ++i) height[i] = h[sa[i]];
剛剛學代碼比較醜,並且可能有問題
#應用 題去這裏找 題去這裏找 ##1.最長公共前綴 題:給定一個字符串,詢問某兩個後綴的最長公共前綴。 分析: 就是區間$height$的最小值,$RMQ$問題工具
重複子串:字符串A在字符串B中至少出現兩次,則稱A是B的重複子串。 ##2.可重疊最長重複子串 題:給定一個字符串,求最長重複子串,重複的兩個子串能夠重疊。 分析: 只須要求 height 數組裏的最大值便可 ##3.不可重疊最長重複子串 題:給定一個字符串,求最長重複子串,重複的兩個子串不能重疊。 分析: 考慮二分答案,每次二分一個答案$k$,把$height$按$>=k$分組 最長公共前綴不小於 k 的兩個後綴必定在同一組。對於每組後綴,只須判斷每一個後綴的 sa 值的最大值和最小值之差是否不小於k便可 另外:利用 height 值對後綴進行分組的方法很經常使用 ##4.可重疊的 k 次最長重複子串 題:給定一個字符串,求至少出現 k 次的最長重複子串,這 k 個子串能夠重疊 分析: 也是二分答案+分組,判斷有沒有一個組的後綴個數不小於 k ##5.不相同的子串的個數 題:給定一個字符串,求不相同的子串的個數。 分析: 每一個子串必定是某個後綴的前綴,也就是求全部後綴不一樣前綴的個數 每來一個後綴$suf(i)$就會有,$len-sa[i]+1$的新的前綴,又因爲有$height$個重複的,那麼就是$len-sa[i]+1-height$的貢獻spa
還有不少用法見下面的文獻.net
#參考文獻 羅穗騫 --算法合集之《後綴數組——處理字符串的有力工具》code