前言
后綴數組這個東西早就有所耳聞,但由於很難,學了好幾遍都沒學會。
最近花了挺長一段時間去研究了一下,總算是勉強學會了用倍增法來實現后綴排序(據說還有一種更快的\(DC3\)法,但是要難得多)。
數組定義
首先,為方便起見,我們用后綴\(_i\)表示從下標\(i\)開始的后綴。(相信大家都知道后綴是什么的)
首先,我們需要定義幾個數組:
\(s\):需要進行后綴排序的字符串。
\(SA_i\):記錄排名為\(i\)的后綴的位置。
\(rk_i\):記錄后綴\(_i\)的排名。
\(pos_i\):同樣記錄排名為\(i\)的后綴的位置。
\(tot_i\):用於基數排序,統計\(i\)的排名。
要注意理解這些數組的定義,這樣才能明白后面的內容。
第一次操作
首先,讓我們來一步步模擬一下第一次操作。
我們第一步是要將每個后綴按照第\(1\)個字符進行排序。
這應該還是比較簡單的,不難發現可以初始化得到\(rk_i=s_i,pos_i=i\)。
然后我們對其進行第一次排序。
注意,排序最好用\(O(n)\)的基數排序,用\(sort\)的話會多一個\(log\)。
具體的一些關於基數排序的細節可以見下。
關於基數排序
后綴排序中的基數排序,其實相當於將二元組\((rk_i,pos_i)\)進行排序。
首先,第一步自然是清空\(tot\)數組。
然后,從\(1\)到\(n\)枚舉,將\(tot_{rk_i}\)加\(1\)。
接下來是一遍累加,求出每一個元素的排名。
然后從\(n\)到\(1\)倒序枚舉,更新\(SA\)數組即可。
接下來的操作
接下來自然是要對每個后綴前\(2\)個字符進行排序了。
暴力的方法就是再重新排序一遍。
但實際上,在確定了第\(1\)個字符的大小關系后,我們就不需要如此麻煩了。
因為后綴\(_i\)的第\(2\)個字符,實際上就是后綴\(_{i+k}\)的第\(1\)個字符。
因此我們通過第一次排序,就可以直接確定第\(2\)個字符的大小關系了。
於是我們就可以重新用\(pos\)數組將這個大小關系記錄下來,再次排序。
然后就是按照這種方法來倍增處理第\(4\)個字符、第\(8\)個字符、第\(16\)個字符... ...
重復此操作直至所有后綴各不相同即可。
這樣的總復雜度就是\(O(nlogn)\)的了。
具體實現還是有很多細節的,實在沒理解的可以根據代碼再研究一下。
代碼(板子題)
class Class_SuffixSort//后綴排序
{
private:
int n,SA[N+5],rk[N+5],pos[N+5],tot[N+5];
inline void RadixSort(int S)//基數排序,S表示字符集大小
{
register int i;
for(i=0;i<=S;++i) tot[i]=0;//清空數組
for(i=1;i<=n;++i) ++tot[rk[i]];//從1到n枚舉,將tot[rk[i]]加1
for(i=1;i<=S;++i) tot[i]+=tot[i-1];//累加
for(i=n;i;--i) SA[tot[rk[pos[i]]]--]=pos[i];//倒序枚舉,更新SA數組
}
public:
inline void Solve(char *s)
{
register int i,k,cnt=0,Size=122;//初始化字符集大小為122(即'z'的ASCII碼)
for(n=strlen(s),i=1;i<=n;++i) rk[pos[i]=i]=s[i-1];//初始化rk數組和pos數組
for(RadixSort(Size),k=1;cnt<n;Size=cnt,k<<=1)//先是一遍基數排序,然后倍增枚舉k,直至所有后綴各不相同
{
for(cnt=0,i=1;i<=k;++i) pos[++cnt]=n-k+i;//將長度小於等於k的后綴先加入數組中,此時的cnt相當於計數器
for(i=1;i<=n;++i) SA[i]>k&&(pos[++cnt]=SA[i]-k);//對於排名大於k的字符串,將其加入數組中
for(RadixSort(Size),i=1;i<=n;++i) pos[i]=rk[i];//基數排序一遍,然后將rk數組的值全部賦值給pos數組
for(rk[SA[1]]=cnt=1,i=2;i<=n;++i) rk[SA[i]]=(pos[SA[i-1]]^pos[SA[i]]||pos[SA[i-1]+k]^pos[SA[i]+k])?++cnt:cnt;//利用SA數組來得到rk,此時的cnt存儲不同的字符串個數,從而得到排名
}
for(i=1;i<=n;++i) F.write(SA[i]),F.writec(' ');//輸出答案
}
}S;