// 本文部分內容參照劉汝佳《演算法競賽入門經典訓練指南》,特此說明。
[20190129更新!] 終於!時隔多年對這篇文章重新整理了一下,感謝大家提出的建議與意見。
1、前言
趁著這幾天上午,把字尾陣列大致看完了。這個東西本身的概念可能沒太大理解問題,但是它所延伸出來的知識很複雜,很多,還有它的兩個兄弟——字尾樹,字尾自動機,編起來都不是蓋的。
2、概念
前面曾經提到過ac自動機(講得有點簡略,它用以解決多模板匹配問題。但是前提是事先知道所有的模板,在實際應用中,我們無法事先知道查詢內容的,比如在搜尋引擎中,你的查詢是不可能直接預處理出來的。這個時候就需要預處理文字串而非每次的查詢內容。
字尾陣列,說的簡單一點,就是將乙個字串的所有字尾儲存起來的陣列,接下來分析它的作用。
3、構建
首先假定乙個字串banana,在後面新增乙個非字母字元「$」,代表乙個沒出現過的標識字元,然後把它的所有字尾——
插入到一棵trie中。由於標識字元的存在,字串每乙個字尾都與乙個葉節點一一對應。如圖所示:
我們發現,有了字尾trie之後,可以o(m)查詢乙個單詞,如右側。
在實際應用中,會把字尾trie中沒有分支的鏈合併在一起,得到所謂的字尾樹,但是由於字尾樹的構造演算法複雜難懂,且容易寫錯,所以在競賽中很少使用,所以暫時不去研究了。相比之下,字尾陣列是必備**,時間效率高,**簡單,而且不易寫錯。
在繪製字尾trie的時候,我們將字典序小的字母排在左邊。由於葉節點和字尾一一對應,我們現在在每乙個葉節點上標上該字尾的首字母在原字串中的位置,如圖:
將所有下標連在一起,構建出來的,就是所謂的字尾陣列了。banana的字尾陣列為sa = ,舉個例子,其中sa[1] = 3表示第3 + 1 = 4個字母開頭的字尾即"ana"在所有字尾中字典序排名為1。這樣的話,我們就可以直接通過一次快速排序o(n log n)得到了。但是,在比較任意兩個字尾時,又需要o(n),故這是o(n^2 log n),根本扛不住。
4、倍增
下面介紹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=,a[i]表示字尾i的名次。而後我們可以得到字尾陣列了:sa=。(你要問我怎麼得到的嘛?)
個人認為,這個思路自己想想還是好些,還是比較清晰的,起碼我是先有思路再看懂網上文章的意思的。
5、基數排序
比較的複雜度為o(log n),如果這個時候再用快速排序的話,依舊需要o(n log^2 n),雖然已經小多了!但是,這個時候如果使用基數排序,可以進一步優化,達到o(n log n)。
首先先來介紹這個以前沒聽過的排序方法。設存在一串行,首先根據個位數的數值,在遍歷資料時將它們各自分配到編號0至9的桶(個位數值與桶號一一對應)中,如下圖左側所示:
得到序列。
再根據十位數排序,如右側,將他們連起來,得到序列。
很好理解的乙個排序。詳細的內容不過多闡述。它的時間複雜度取決於數的多少以及數的位數。
在構建字尾陣列的過程中,我們可以發現最大位數為2(字母總共只有26個),用基數排序的複雜度明顯小於快速排序。
下面給出乙個臨時的字尾陣列構建模板,可以發現很多地方的模板都長這個樣子的。
1 #include 2【對如上**的注釋】using
namespace
std;34
#define maxn 1005
5#define maxm 3067
char
ch[maxn];
8int sa[maxn], a[maxn], t[maxn], c[maxn], n, m =maxm, p;910
intmain()
30return0;
31 }
n表示串的長度,m表示字元種類數。由於m沒有直接給出,故初始賦值為30(大於可能出現的字元種類個數即可)。
6、最長公共字首
目前我們得到的只有字尾陣列乙個東西。接下來就有一系列的延伸。比如說,在o(n log n)的時間內處理最長公共字首,即lcp。求n個字串lcp,暴力需要o(n^3),完全不是乙個級別。
而利用字尾陣列的話,通常需要兩個陣列,rank[i]表示字尾i在sa陣列中的下標;height[i]表示sa[i-1]和sa[i]的最長公共字首長度。對於兩個字首j和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下面是該優化的證明:intrank[maxn], height[maxn];23
void
geth()
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。
7、總結
這是乙個非常高大上的東西,也許說這些看起來還是易懂的,但是題目做起來還是能夠達到一種境界的。尤其還有字尾自動機等內容沒有提。我認為字尾陣列其實是個很巧妙的東西,更何況加在上面的各種優化。
陣列知識點
陣列就是按順序排列的一組同種型別的變數構成的集合 佔一片連續的儲存單元 陣列元素 下標變數 本質是變數。一 一維陣列 1.格式 陣列名 常量表示式 2.初始化 陣列定義後的初值仍然是隨機數 如 int a 5 int a 10 該方法僅對陣列的前五個元素依次進行初始化,其餘值為0。int a 5 表...
陣列知識點
1.通過以下 塊,看出普通陣列與引用型別陣列區別 public class test01 for int i 0 i user arr02 newuser 3 引用型別的陣列 arr02 0 new user 1 肖杰航 arr02 1 new user 2 小傑航 arr02 2 new user...
陣列知識點
陣列就是乙個容器,用於存放一系列相同的資料型別 乙個變數名存放多個資料 1 宣告陣列 int a 兩種方式 int a1 int a2 2 分配空間 a new int 5 3 賦值 a 0 8 4 處理資料 3 擴充套件點 增強的for迴圈 input.next 與 input.nextline ...