1. 程式人生 > 其它 >學習python -- 第018天 多程序

學習python -- 第018天 多程序

字尾陣列

關鍵是如何得到sa陣列。

採用倍增和基數排序的方法。

因為字符集一般較小, 所以基數排序是首選。

關鍵是如何倍增處理。 其中的關鍵思路是二元組遞推排序。

二元組排序

考慮多個二元組(x, y), 按照以x為第一關鍵字, 以y為第二關鍵字排序。 可以先按y排序, 後按x排序。

倍增過程

字典序的比較是從前向後不斷進行的, 因此不斷倍增所有後綴字首的長度。 然後進行排序。

考慮當前要求字尾的字首長度為k時後綴的排名。 將字首分為兩部分, 構成一個二元組。

先給後部分排序。 如果後部分不存在, 則放在前面。 然後由sa得到剩下的字尾的排序。 同時將編號向前調整。

之後給字首排序, 排完序後得到sa。

然後將二元組一一對映, 得到新的排序依據。

具體實現關鍵是x陣列和y陣列。 x陣列存的是上一輪字首排名的對映值, y為按第二關鍵字,即當前處理字串的字尾排序後的字尾編號, 可以直接由sa得到。

具體流程如下:

  1. 將一個字元排序, x的對映值之間設為字元值即可, 然後用基數排序得到字首為1的sa排名。
  2. 將要排序的字首長度調整擴大二倍, 首先排字首的後一半。 先把後一半長度不夠的放在前面, 前一半的根據sa陣列直接排, 將編號大於k的編號減少k, 調整到字首的起點。
  3. 之後給字首的前一半基數排序。 由於後一半已經有序, 只關心前一半即可。
  4. 根據sa更新x陣列, 因為sa內部是有序的, 如果sa[i]和sa[i-1]的前半部分字尾和後半部分字尾相同, 排名設定也相同, 否則排名加一。
  5. 判斷排名是否到了n, 如果沒有說明有字首相同的, 便繼續第2步。

具體流程便是倍增長度, 排後半部分, 排前半部分, 更新排序依據, 判斷是否結束。

更多細節見程式碼註釋。

程式碼來自《演算法競賽入門經典——訓練指南》, 建議看著書上的圖閱讀程式碼, 程式碼註釋中會以該圖為例詳細解釋。

void build_sa(int m) {
    //PS:
    //過程是不斷對所有後綴的字首排序, 且將字尾分為長度相等的字首和字尾, 所以下面統一簡稱這兩部分為字首與字尾。

    //x陣列為已處理部分的排序對映。即書中最頂上那一組數。不過是從0開始。
    //y陣列為按照第二關鍵字,即按字首的後半部分排序後的字尾編號。 初始可視為 y[i] = i;
	int i, *x = t, *y = t2;
	
    //首先處理字首為1, c陣列用於為基數排序。
	for(i = 0; i < m; i++) c[i] = 0;
	for(i = 0; i < n; i++) c[x[i] = s[i]]++;//這裡無論字符集是否確定,都這麼寫就可以,不過m要足夠大。 後面會統一對映到0-n。
	for(i = 1; i < m; i++) c[i] += c[i-1];
	for(i = n-1; i >= 0; i--) sa[--c[x[i]]] = i;
	
    //可以將註釋的程式碼取消註釋,對照書中的圖詳細觀察。 不過x書中不會和書中一樣,因為從0開始編號。 不過對結果和過程理解不會影響。
	// puts("0\nsa: ");
	// for(i = 0; i < n; i++) printf("%d ", sa[i]);
	// puts("");
	
	for(int k = 1; k <= n; k <<= 1) {
		int p = 0;
		
		// printf("\n%d\n", k);
		
		// printf("x: ");
		// for(i = 0; i < n; i++) printf("%d ", x[i]);
		
        //按照第二關鍵字,即字尾排序。
        //後面長度不夠的放在前面,也就是所謂的“補0”。
		for(i = n-k; i < n; i++) y[p++] = i;
        //在上一輪已經處理了排名, 直接拿來用,不過當前排序的是字尾, 因此編號要向前調整k。
		for(i = 0; i < n; i++) if(sa[i] >= k) y[p++] = sa[i]-k;
		
        //這裡對照書的圖例,y[i]已經按照後半部分排好序。
		// printf("\ny %d:", p);
		// for(i = 0; i < p; i++) printf("%d ", y[i]);
		// printf("\nx[y[i]]:");
		// for(i = 0; i < n; i++) printf("%d ", x[y[i]]);
		
        //給字首排序,得到最終排名
		for(i = 0; i < m; i++) c[i] = 0;
		for(i = 0; i < n; i++) c[x[y[i]]]++;
		for(i = 0; i < m; i++) c[i] += c[i-1];
		for(i = n-1; i >= 0; i--) sa[--c[x[y[i]]]] = y[i];
		
		// printf("\nsa: ");
		// for(i = 0; i < n; i++) printf("%d ", sa[i]); 
		
		//需要用x陣列和sa陣列更新x陣列, y陣列已經無用, 用來備份x陣列。
        //排名從0開始,二元組相同排名相同,否則排名增加。 因為sa中已經有序。
		swap(x, y);
		p = 1; x[sa[0]] = 0;
		for(i = 1; i < n; i++) {
			x[sa[i]] = y[sa[i-1]] == y[sa[i]] && y[sa[i-1]+k] == y[sa[i]+k] ? p-1: p++;
		}
		if(p >= n) break;
		m = p;
	}
}