// 本文部份內容參照劉汝佳《算法競賽入門經典訓練指南》,特此說明。html
[20190129更新!] 終於!時隔多年對這篇文章從新整理了一下,感謝你們提出的建議與意見。c++
一、前言算法
趁着這幾天上午,把後綴數組大體看完了。這個東西自己的概念可能沒太大理解問題,可是它所延伸出來的知識很複雜,不少,還有它的兩個兄弟——後綴樹,後綴自動機,編起來都不是蓋的。數組
二、概念優化
前面曾經提到過AC自動機(http://www.cnblogs.com/jinkun113/p/4682853.html),講得有點簡略,它用以解決多模板匹配問題。可是前提是事先知道全部的模板,在實際應用中,咱們沒法事先知道查詢內容的,好比在搜索引擎中,你的查詢是不可能直接預處理出來的。這個時候就須要預處理文本串而非每次的查詢內容。搜索引擎
後綴數組,說的簡單一點,就是將一個字符串的全部後綴儲存起來的數組,接下來分析它的做用。 spa
三、構建3d
首先假定一個字符串BANANA,在後面添加一個非字母字符「$」,表明一個沒出現過的標識字符,而後把它的全部後綴——code
插入到一棵Trie中。因爲標識字符的存在,字符串每個後綴都與一個葉節點一一對應。如圖所示:htm
咱們發現,有了後綴Trie以後,能夠O(m)查找一個單詞,如右側。
在實際應用中,會把後綴Trie中沒有分支的鏈合併在一塊兒,獲得所謂的後綴樹,可是因爲後綴樹的構造算法複雜難懂,且容易寫錯,因此在競賽中不多使用,因此暫時不去研究了。相比之下,後綴數組是必備武器,時間效率高,代碼簡單,並且不易寫錯。
在繪製後綴Trie的時候,咱們將字典序小的字母排在左邊。因爲葉節點和後綴一一對應,咱們如今在每個葉節點上標上該後綴的首字母在原字符串中的位置,如圖:
將全部下標連在一塊兒,構建出來的,就是所謂的後綴數組了。BANANA的後綴數組爲sa[] = {5, 3, 1, 0, 4, 2},舉個例子,其中sa[1] = 3表示第3 + 1 = 4個字母開頭的後綴即"ANA"在全部後綴中字典序排名爲1。這樣的話,咱們就能夠直接經過一次快速排序O(n log n)獲得了。可是,在比較任意兩個後綴時,又須要O(n),故這是O(n^2 log n),根本扛不住。
四、倍增
下面介紹Manber和Myers發明的倍增算法,時間複雜度O(n log n)(不採用基數排序的話就是O(n log^2 n))。
首先對於全部單個字符排序(也能夠理解成對於每個後綴的第1個字符排序,這樣後面的步驟更易銜接),如圖:
對於每一個字母,咱們根據字典序給予其一個名次,則a->1,b->2,n->3。
而接下來,咱們再給全部後綴的前兩個字符排序(以前就是前一個),將相鄰二元組合並,再次根據字典序給予一個名次,如圖:
而咱們如今獲得了全部後綴的前2個字符的排名,注意這種方法是倍增思想,接下來要求的就是全部後綴的前4個字符的名次,由於可知對於後綴x的前4個字符是由後綴x的前2個字符和後綴x+2的前2個字符組成的,方法同上。如圖:
咱們也能夠注意到,當咱們試圖再去把全部後綴的前8個字符排一遍序的時候會發現,並無任何含義。首先,這個字符串的長度沒有達到8,其次全部名詞已經兩兩不一樣,已經達到了咱們的目的。因此咱們能夠分析出,這個過程的時間複雜度穩定爲O(log n)。
獲得了序列a[]={4,3,6,2,5,1},a[i]表示後綴i的名次。然後咱們能夠獲得後綴數組了:sa[]={5,3,1,0,4,2}。(你要問我怎麼獲得的嘛?)
我的認爲,這個思路本身想一想仍是好些,仍是比較清晰的,起碼我是先有思路再看懂網上文章的意思的。
五、基數排序
比較的複雜度爲O(log n),若是這個時候再用快速排序的話,依舊須要O(n log^2 n),雖然已經小多了!可是,這個時候若是使用基數排序,能夠進一步優化,達到O(n log n)。
首先先來介紹這個之前沒聽過的排序方法。設存在一序列{73,22,93,43,55,14,28,65,39,81},首先根據個位數的數值,在遍歷數據時將它們各自分配到編號0至9的桶(個位數值與桶號一一對應)中,以下圖左側所示:
獲得序列{81,22,73,93,43,14,55,65,28,39}。再根據十位數排序,如右側,將他們連起來,獲得序列{14,22,28,39,43,55,65,73,81,93}。
很好理解的一個排序。詳細的內容不過多闡述。它的時間複雜度取決於數的多少以及數的位數。
在構建後綴數組的過程當中,咱們能夠發現最大位數爲2(字母總共只有26個),用基數排序的複雜度明顯小於快速排序。下面給出一個臨時的後綴數組構建模板,能夠發現不少地方的模板都長這個樣子的。
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 #define MAXN 1005 5 #define MAXM 30 6 7 char ch[MAXN]; 8 int sa[MAXN], a[MAXN], t[MAXN], c[MAXN], n, m = MAXM, p; 9 10 int main() {
11 scanf("%s", ch), n = strlen(ch); 12 for (int i = 0; i < n; i++) c[a[i] = (ch[i] - 'a' + 1)]++; 13 for (int i = 1; i < m; i++) c[i] += c[i - 1]; 14 for (int i = n - 1; i >= 0; i--) 15 sa[--c[a[i]]] = i; 16 for (int k = 1; k <= n; k <<= 1) { 17 int p = 0; 18 for (int i = n - k; i < n; i++) t[p++] = i; 19 for (int i = 0; i < n; i++) if (sa[i] >= k) t[p++] = sa[i] - k; 20 for (int i = 0; i < m; i++) c[i] = 0; 21 for (int i = 0; i < n; i++) c[a[t[i]]]++; 22 for (int i = 0; i < m; i++) c[i] += c[i - 1]; 23 for (int i = n - 1; i >= 0; i--) sa[--c[a[t[i]]]] = t[i]; 24 swap(a, t); 25 p = 1, a[sa[0]] = 0; 26 for (int i = 1; i < n; i++) a[sa[i]] = (t[sa[i - 1]] == t[sa[i]] && t[sa[i - 1] + k] == t[sa[i] + k]) ? p - 1 : p++; 27 if (p >= n) break; 28 m = p; 29 } 30 return 0; 31 }
【對如上代碼的註釋】
n表示串的長度,m表示字符種類數。因爲m沒有直接給出,故初始賦值爲30(大於可能出現的字符種類個數便可)。
六、最長公共前綴
目前咱們獲得的只有後綴數組一個東西。接下來就有一系列的延伸。好比說,在O(n log n)的時間內處理最長公共前綴,即LCP。求n個字符串LCP,暴力須要O(n^3),徹底不是一個級別。
而利用後綴數組的話,一般須要兩個數組,rank[i]表示後綴i在SA數組中的下標;height[i]表示sa[i-1]和sa[i]的最長公共前綴長度。對於兩個前綴j和k,j<k,不妨設rank[j]<rank[k]。不可貴到,後綴j和k的LCP長度等於height[rank[j+x]](x∈[1,k-j])中的最小值,舉一個例子就能明白。
好仍是好理解的,可是想一想,根據定義,每次計算一對的height數組,都須要O(n),則共須要O(n^2),這顯然讓人感到不可忍,畢竟構建SA數組的時候都只須要O(n log n)。
然而這個時候咱們再用個輔助數組a[i]=height[rank[i]],而後按照h[1],h[2]……h[n]的順序遞推計算。遞推的關鍵在於這樣一個性質:h[i]>=h[i-1]-1.這樣就不須要從字符串開頭計算了。以下方。
代碼:
1 int rank[MAXN], height[MAXN]; 2 3 void geth() { 4 for (int i = 0; i < n; i++) rank[sa[i]] = i; 5 for (int i = 0; i < n; i++) { 6 if (k) k--; 7 int j = sa[rank[i] - 1]; 8 while (ch[i + k] == ch[j + k]) k++; 9 height[rank[i]] = k; 10 } 11 }
下面是該優化的證實:
設排在後綴i-1前一個的是後綴k。後綴k和後綴i-1分別刪除首字符以後獲得後綴k+1和後綴i,所以後綴k+1必定排在後綴i的前面,而且最長公共點綴長度爲h[i-1]-1,如圖所示:
這個h[i-1]-1是一系列h值的最小值,這些h值包括後綴i和排在它前一個的後綴p的LCP長度,即h[i]。所以h[i]>=h[i-1]-1。
七、總結
這是一個很是高大上的東西,也許說這些看起來仍是易懂的,可是題目作起來仍是可以達到一種境界的。尤爲還有後綴自動機等內容沒有提。我認爲後綴數組實際上是個很巧妙的東西,更況且加在上面的各類優化。