后綴排序


后綴排序

讀入一個長度為 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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM