1. 程式人生 > >字尾陣列詳解

字尾陣列詳解

字尾陣列學習筆記【詳解】

老天,一個字尾陣列不知道看了多少天,最後終於還是看懂了啊!

最關鍵的就是一會兒下標表示排名,一會用數值表示排名繞死人了。

我不知道手跑了多少次才明白過來。其實我也建議初學者手跑幾遍,但是一定要注意陣列的意義,否則就是無用功。

陣列含義:

s[ ]:輸入的字串,預處理的時候會在末尾加上一個0

sa[ ]:它的下標就是字尾排名

x[ ] = t[ ]:用來儲存第一關鍵字排名,注意!它的數值是排名。初始時恰好是字串的ASCII碼。字典序嘛!

y[ ] = t2[ ]:它的下標就是第二關鍵字排名,第二關鍵字是直接從sa[ ]當中提取的,關係極其密切

c[ ]:用來基數排序。初始值恰好是每種字元出現的次數。後來它的作用就跟基數排序密切相關,建議學習基數排序

有一點一定要注意!第二關鍵字來自sa[ ]陣列,但是第一關鍵字並不是來自sa[ ]陣列!這一點不知道迷惑了多少人,就是因為論文裡給出的圖完全就是原理圖,不是程式碼實現的圖,不搭噶的!

P.S. 為了優化時間空間,避免新開一箇中間陣列來複制t[ ]的值,採用了將它的指標x和t2[ ]的指標y交換的方法。注意這個時候t2[ ]已經沒有用了。

我先給出一個足以理解字尾陣列的增加了中間輸出的程式碼:

[cpp] view plain copy  print?
  1. #include <cstdio>
  2. #include <cstring>
  3. #include <algorithm>
  4. usingnamespace std;  
  5. constint N = 1000, M = 130;  
  6. char s[N];  
  7. int sa[N], t[N], t2[N], c[M], n;  
  8. int rank[N], high[N];  
  9. #define DBG
  10. #ifdef DBG
  11. int db[N];  
  12. void debug(int *f)  
  13. {  
  14.     for(int i = 0; i < n; i++) {  
  15.         db[f[i]] = i;  
  16.     }  
  17.     printf("%3d", db[0]);  
  18.     for(int i = 1; i < n; i++) {  
  19.         printf(" %3d", db[i]);  
  20.     }puts("]");  
  21. }  
  22. #endif
  23. bool cmp(int *y, int i, int k)  
  24. {  
  25.     return y[sa[i-1]] == y[sa[i]] && y[sa[i-1]+k] == y[sa[i]+k];  
  26. }  
  27. void build(int m)  
  28. {  
  29.     int i, *x = t, *y = t2;  
  30.     for(i = 0; i < m; i++) c[i] = 0;  
  31.     for(i = 0; i < n; i++) c[x[i] = s[i]]++;  
  32.     for(i = 1; i < m; i++) c[i] += c[i-1];  
  33.     for(i = n-1; i >= 0; i--) sa[--c[x[i]]] = i;  
  34. #ifdef DBG
  35.     printf("sa Get:[");  
  36.     debug(sa);  
  37.     puts("");  
  38. #endif
  39.     for(int k = 1, p; k <= n; k<<=1, m=p) {  
  40.         p = 0;  
  41.         //y[]的下標就是對應的第二關鍵字排名,它是由sa[]直接得來的
  42.         //另外y[]的內容就是第一關鍵字所在位置
  43.         for(i = n-k; i < n; i++) y[p++] = i;  
  44.         for(i = 0; i < n; i++) if(sa[i] >= k) y[p++] = sa[i] - k;  
  45. #ifdef DBG
  46.         printf("Gain y:[");  
  47.         debug(y);  
  48.         printf("Look x:{");  
  49.         printf("%3d", x[0]);  
  50.         for(i = 1; i < n; i++) {  
  51.             printf(" %3d", x[i]);  
  52.         }puts("}");  
  53. #endif
  54.         //x[]的內容就是對應的第一關鍵字排名
  55.         //根據x[]的內容和y[]的下標進行合併,得到新的排名作為sa[]的下標
  56.         for(i = 0; i < m; i++) c[i] = 0;  
  57.         for(i = 0; i < n; i++) c[x[y[i]]]++;  
  58.         for(i = 1; i < m; i++) c[i] += c[i-1];  
  59.         for(i = n-1; i >= 0; i--) sa[--c[x[y[i]]]] = y[i];  
  60. #ifdef DBG
  61.         printf("sa Get:[");  
  62.         debug(sa);  
  63.         puts("");  
  64. #endif
  65.         //按照sa[]的順序提取出老的x[],計算新的x[]
  66.         swap(x, y);  
  67.         p = 1; x[sa[0]] = 0;//sa[0]一定是新增的字元0,排名萬年第0
  68.         for(i = 1; i < n; i++) {  
  69.             x[sa[i]] = cmp(y, i, k) ? p-1 : p++;  
  70.         }  
  71.         //剪枝,此時x[]中已經沒有相同的值,sa[]被確定
  72.         if(p >= n) break;  
  73.     }  
  74. }  
  75. void get_high()  
  76. {  
  77.     int k = 0;  
  78.     for(int i = 0; i < n; i++) rank[sa[i]] = i;  
  79.     for(int i = 0; i < n; i++) {  
  80.         if(k) k--;  
  81.         int j = sa[rank[i]-1];  
  82.         while(s[i+k] == s[j+k]) k++;  
  83.         high[rank[i]] = k;  
  84.     }  
  85. }  
  86. void PR()  
  87. {  
  88.     printf("The rank is:\n");  
  89.     printf("%d", rank[0]);  
  90.     for(int i = 1; i < n-1; i++) printf(" %d", rank[i]);  
  91.     puts("");  
  92. }  
  93. int main()  
  94. {  
  95.     scanf("%s", s);  
  96.     n = strlen(s) + 1;  
  97.     int maxi = 0;  
  98.     for(int i = 0; i < n; i++) {  
  99.         maxi = maxi > s[i] ? maxi : s[i];  
  100.     }  
  101.     s[n-1] = 0;  
  102.     build(maxi+1);  
  103.     get_high();  
  104. #ifdef DBG
  105.     PR();  
  106. #endif
  107.     return 0;  
  108. }  

根據這份程式碼,輸入一些資料測試一下,仔細研究研究中間輸出。

建議資料:

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[ ]陣列吧!