字尾陣列詳解
字尾陣列學習筆記【詳解】
老天,一個字尾陣列不知道看了多少天,最後終於還是看懂了啊!
最關鍵的就是一會兒下標表示排名,一會用數值表示排名繞死人了。
我不知道手跑了多少次才明白過來。其實我也建議初學者手跑幾遍,但是一定要注意陣列的意義,否則就是無用功。
陣列含義:
s[ ]:輸入的字串,預處理的時候會在末尾加上一個0
sa[ ]:它的下標就是字尾排名
x[ ] = t[ ]:用來儲存第一關鍵字排名,注意!它的數值是排名。初始時恰好是字串的ASCII碼。字典序嘛!
y[ ] = t2[ ]:它的下標就是第二關鍵字排名,第二關鍵字是直接從sa[ ]當中提取的,關係極其密切
c[ ]:用來基數排序。初始值恰好是每種字元出現的次數。後來它的作用就跟基數排序密切相關,建議學習基數排序
有一點一定要注意!第二關鍵字來自sa[ ]陣列,但是第一關鍵字並不是來自sa[ ]陣列!這一點不知道迷惑了多少人,就是因為論文裡給出的圖完全就是原理圖,不是程式碼實現的圖,不搭噶的!
P.S. 為了優化時間空間,避免新開一箇中間陣列來複制t[ ]的值,採用了將它的指標x和t2[ ]的指標y交換的方法。注意這個時候t2[ ]已經沒有用了。
我先給出一個足以理解字尾陣列的增加了中間輸出的程式碼:
[cpp] view plain copy print?- #include <cstdio>
- #include <cstring>
-
#include <algorithm>
- usingnamespace std;
- constint N = 1000, M = 130;
- char s[N];
- int sa[N], t[N], t2[N], c[M], n;
- int rank[N], high[N];
- #define DBG
- #ifdef DBG
- int db[N];
- void debug(int *f)
- {
- for(int i = 0; i < n; i++) {
- db[f[i]] = i;
- }
- printf("%3d", db[0]);
-
for(int i = 1; i < n; i++) {
- printf(" %3d", db[i]);
- }puts("]");
- }
- #endif
- bool cmp(int *y, int i, int k)
- {
- return y[sa[i-1]] == y[sa[i]] && y[sa[i-1]+k] == y[sa[i]+k];
- }
- void build(int m)
- {
- int i, *x = t, *y = t2;
- for(i = 0; i < m; i++) c[i] = 0;
- for(i = 0; i < n; i++) c[x[i] = s[i]]++;
- for(i = 1; i < m; i++) c[i] += c[i-1];
- for(i = n-1; i >= 0; i--) sa[--c[x[i]]] = i;
- #ifdef DBG
- printf("sa Get:[");
- debug(sa);
- puts("");
- #endif
- for(int k = 1, p; k <= n; k<<=1, m=p) {
- p = 0;
- //y[]的下標就是對應的第二關鍵字排名,它是由sa[]直接得來的
- //另外y[]的內容就是第一關鍵字所在位置
- for(i = n-k; i < n; i++) y[p++] = i;
- for(i = 0; i < n; i++) if(sa[i] >= k) y[p++] = sa[i] - k;
- #ifdef DBG
- printf("Gain y:[");
- debug(y);
- printf("Look x:{");
- printf("%3d", x[0]);
- for(i = 1; i < n; i++) {
- printf(" %3d", x[i]);
- }puts("}");
- #endif
- //x[]的內容就是對應的第一關鍵字排名
- //根據x[]的內容和y[]的下標進行合併,得到新的排名作為sa[]的下標
- for(i = 0; i < m; i++) c[i] = 0;
- for(i = 0; i < n; i++) c[x[y[i]]]++;
- for(i = 1; i < m; i++) c[i] += c[i-1];
- for(i = n-1; i >= 0; i--) sa[--c[x[y[i]]]] = y[i];
- #ifdef DBG
- printf("sa Get:[");
- debug(sa);
- puts("");
- #endif
- //按照sa[]的順序提取出老的x[],計算新的x[]
- swap(x, y);
- p = 1; x[sa[0]] = 0;//sa[0]一定是新增的字元0,排名萬年第0
- for(i = 1; i < n; i++) {
- x[sa[i]] = cmp(y, i, k) ? p-1 : p++;
- }
- //剪枝,此時x[]中已經沒有相同的值,sa[]被確定
- if(p >= n) break;
- }
- }
- void get_high()
- {
- int k = 0;
- for(int i = 0; i < n; i++) rank[sa[i]] = i;
- for(int i = 0; i < n; i++) {
- if(k) k--;
- int j = sa[rank[i]-1];
- while(s[i+k] == s[j+k]) k++;
- high[rank[i]] = k;
- }
- }
- void PR()
- {
- printf("The rank is:\n");
- printf("%d", rank[0]);
- for(int i = 1; i < n-1; i++) printf(" %d", rank[i]);
- puts("");
- }
- int main()
- {
- scanf("%s", s);
- n = strlen(s) + 1;
- int maxi = 0;
- for(int i = 0; i < n; i++) {
- maxi = maxi > s[i] ? maxi : s[i];
- }
- s[n-1] = 0;
- build(maxi+1);
- get_high();
- #ifdef DBG
- PR();
- #endif
- return 0;
- }
根據這份程式碼,輸入一些資料測試一下,仔細研究研究中間輸出。
建議資料:
abaab
aabaaaab
banana
接下來是手跑過程:
方框代表裡面的值是下標,花括號代表是數值。它們都是和第一行紅色數字一一對應的。
我們暫時不去管第一關鍵字是怎樣計算出來的。
根據上面的程式,自己來填寫這張圖當中的數值。一個一個填寫就可以明白了。(x[ ]陣列的值就直接看圖上的,並且注意每一個x[ ]陣列都是在上一層基數排序計算出來的)
sa[ ]的初始值恰好是根據字元出現次數一個一個來的,輕易就可以手跑出來。這就完成了一位數的基數排序。
藍色的字是第二關鍵字,正好是從sa[ ]當中提取出來的。黃色的箭頭表示沒有第二關鍵字,它們的排名是自左向右從0開始填的,要先填完這個再提取其他的第二關鍵字。再次強調,雖然有線,但是第一關鍵字並不是sa[ ]陣列當中的數!
然後給出的x[ ]和剛填完的y[ ]合併(綠色字型),計算出sa[ ]。這是兩位數的基數排序。
接下來繼續倍增,完成四位數的基數排序。(如果你困惑為什麼還是隻有兩個數被線指著,建議閱讀論文)
最後,其實本來是不用對八位數進行基數排序,因為這個時候新的x[ ]陣列(圖中倒數第二行)裡面已經沒有重複的排名了,而第一關鍵字是首要的,因此sa[ ]陣列被確定下來了。這裡可以加個剪枝,break一下。
怎樣得到x[ ]陣列:
在每一次得到sa[ ]陣列之後,計算新的x[ ],方法是按照sa[ ]當中的排名順序,(即sa[1...n])提取出舊的x[ ](注意此時它的名字叫做y[ ]了)來計算。如果某字串跟之前的那個完全一樣(即cmp()函式),排名就一樣(p-1)。
根據上面的話,再來自己填寫x[ ]陣列吧!