后綴排序
讀入一個長度為 n 的由大小寫英文字母或數字組成的字符串,請把這個字符串的所有非空后綴按字典序從小到大排序,然后按順序輸出后綴的第一個字符在原串中的位置。位置編號為 1 到 n 。\(n<=10^6\)。
https://blog.csdn.net/Bule_Zst/article/details/78604864
首先,倍增法后綴排序,就相當於進行logn次對兩位數的基數排序。至於為什么要在每一層中基數排序,這是因為要把每一層后綴的名次求出來,這樣才能保證以后基數排序時,桶的大小不超過n。
重要的是如何實現:
const int maxn=1e6+5;
char s[maxn];
int n, m=maxn, r[maxn], sa[maxn];
int *x, *y, *t, wa[maxn], wb[maxn], ws[maxn], wv[maxn], ht[maxn];
int cmp(int *r, int a, int b, int l){
return r[a]==r[b]&&r[a+l]==r[b+l]; }
void Ssort(int *r){
x=wa; y=wb; m=maxn;
int i, j, p=0;
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=0; i<n; ++i) sa[--ws[r[i]]]=i; //sa數組必須排好序
for (j=1; j<n&&p<n; j<<=1, m=p+1){ //p代表當前倍增情況下有多少不同的后綴 m應當變成p+1
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]]; //wv:第二關鍵詞中排i的數,在第一關鍵詞中排第幾
for (i=0; i<m; ++i) ws[i]=0;
for (i=0; i<n; ++i) ++ws[x[i]]; //ws:第一關鍵詞中排名為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];
t=x; x=y; y=t; x[sa[0]]=1; //x要開始接受新的排名了
for (p=1, i=1; i<n; ++i) //rank必須從1開始以區分空串
x[sa[i]]=cmp(y, sa[i-1], sa[i], j)?p:++p; //這句話看上去很可怕,其實只是判斷當前后綴和前一個后綴是否相同而已
}
for (i=0; i<n; ++i) --x[i]; p=0;
for (i=0; i<n; ht[x[i++]]=p){ //枚舉原串中1到n的所有后綴
if (!x[i]){ p=0; continue; }
for (p?p--:0, j=sa[x[i]-1]; r[i+p]==r[j+p]&&i+p<n; ++p); //p表示h[i]
} return;
}
sa數組的含義是:在第一關鍵詞中,排第幾的是誰?(也就是后綴數組)
x數組的含義是:在第一關鍵詞中,你是第幾?(也就是名次數組)
y數組的含義是:在第二關鍵詞中,排第幾的是誰?
倍增模塊外ws[i]的含義:i這個數的最大排名。
倍增模塊內ws[i]的含義:第一關鍵詞中排名為i的數,在總排名中的最大排名。
wv[i]的含義:在第二關鍵詞中排第i的數,在第一關鍵詞中排第幾?
ws[wv[i]]的含義:在第二關鍵詞中排第i的數,在總排名中的最大排名。
由於\(sa[i]\)表示排第i位的是哪個后綴,因此即使有兩個后綴相同,它們的排名也不能相同,不然無法在sa數組中表示。
來看看代碼。進入倍增之前,先把原始的sa數組和名次數組求出來。進入倍增后,先求出對於第二關鍵詞的后綴數組y,然后利用y數組求出數組wv:第二關鍵詞中排第i的數,在第一關鍵詞中排第幾。接着利用ws[wv[i]]就可以在后綴按第一關鍵詞已經排好序的前提下,將后綴按第二關鍵詞的順序填充到桶里。
要記住,后綴排序算法中,利用了wv和ws兩個中轉數組。把它們兩個的含義記下來,就會好理解很多。
但是,uoj的板子里面還有一個要求:
除此之外為了進一步證明你確實有給后綴排序的超能力,請另外輸出 n−1 個整數分別表示排序后相鄰后綴的最長公共前綴的長度。
排在第i位的后綴與第i-1位的后綴的最長公共前綴(LCP)定義為\(height[i]\)。定義\(h[i]=height[x[i]]\),直觀上理解,就是第i個后綴與排在它前一位的那個后綴的LCP。uoj這里要我們求的是height數組。那我們還要h數組干嘛呢?原因是有這樣的性質:\(h[i]>=h[i-1]-1\)。畫個圖可以直觀的理解:
上面兩個串的藍色部分表示\(h[i-1]\),那么顯然,下面的藍色部分\(h[i]\)是\(h[i-1]-1\),除非\(sa[x[i-1]-1]\)並不是原來那個\(sa[x[i]-1]\)那個串變成的,而是另外一個字典序比\(sa[x[i]-1]\)大的串,那么此時\(h[i]>h[i-1]-1\)。因此,我們按照在原串中從小到大的位置處理\(height[x[i]]\)。
- 若\(x[i]=1\),即這個后綴的字典序是最小的,那么\(height[x[i]]=height[1]\)只能為0。不用比較
- 若\(i=1\),或者\(h[i-1]<=1\),那么需要暴力計算\(height[x[i]]\),由於最多比較\(h[i]+1\)次,因此比較次數不超過\(h[i]-h[i-1]+2\)。
- 如果以上兩種情況都不滿足,那么\(S_i\)和\(S_{i-1}\)的前驅至少有\(h[i-1]-1\)個字符相同。因此字符比較只需要從\(h[i-1]\)開始,知道某個字符不相同,計算出\(h[i]\)。比較次數正好為\(h[i]-h[i-1]+2\)。
因此,總比較次數為\(h[n]+2*n\),時間復雜度為\(O(n)\)。所以算法的總時間復雜度為\(O(nlogn)\)。
Tip:遇到業界良心(獨留)uoj的類似於“aaaaaaaaaaaaaaaa”的測試數據,我猜洛谷上不少人的代碼會炸(包括我在之前寫的程序)。大家可以去uoj上交一發。
PS:若只需要對字符串兩兩lcp,則只需要trie。