后綴樹 & 后綴數組


后綴樹:

字符串匹配算法一般都分為兩個步驟,一預處理,二匹配。

KMP和AC自動機都是對模式串進行預處理,后綴樹和后綴數組則是對文本串進行預處理。

后綴樹的性質

  • 存儲所有 n(n-1)/2 個后綴需要 O(n) 的空間,n 為的文本(Text)的長度;
  • 構建后綴樹需要 O(dn) 的時間,d 為字符集的長度(alphabet);
  • 對模式(Pattern)的查詢需要 O(dm) 時間,m 為 Pattern 的長度;

介紹后綴樹之前,我們首先要知道壓縮字典樹的概念。

我們在對關鍵字建立字典樹的時候,有時某些節點只會有一個子樹,沒有別的支路。那么這個節點和他的子樹就可以壓縮成一個節點。

比如,下面是集合 {bear, bell, bid, bull, buy, sell, stock, stop} 所構建的 Trie 樹。

他對應的壓縮字典樹就是這樣的。

 

而后綴樹就是一棵以所有的后綴為關鍵字建立的壓縮字典樹

比如,對於文本 "banana\0",其中 "\0" 作為文本結束符號。下面是該文本所對應的所有后綴。

banana\0
anana\0
nana\0
ana\0
na\0
a\0
\0

將每個后綴作為一個關鍵詞,構建一棵 Trie。

然后,將獨立的節點合並,形成 Compressed Trie。

則上面這棵樹就是文本 "banana\0" 所對應的后綴樹。

像上面建立的其實是一棵顯式后綴樹,因為如果我們不在后綴后面加上\0.某些后綴比如a,na在其他后綴的前綴中出現過,他們會被表示在一條路徑上,這樣的后綴樹被稱為隱式后綴樹

在后綴后加入一個后綴中沒有出現過的字符,如\0或是$可以保證后綴的唯一性,此時建立的就是一種顯式后綴樹

 

后綴樹 與 字典樹 的不同在於,邊(Edge)不再只代表單個字符,而是通過一對整數 [from, to] 來表示。其中 from 和 to 所指向的是 Text 中的位置,這樣每個邊可以表示任意的長度,而且僅需兩個指針,耗費 O(1) 的空間。

 

 

后綴數組:

后綴樹的建立相當的麻煩,后綴數組的提出就是為了代替后綴樹,並且后綴數組節省了空間。因此題目中我們大多實現后綴數組。

 

其與后綴樹的的關系有:

 

  • 后綴數組可以通過對后綴樹做深度優先遍歷(DFT: Depth First Traversal)來進行構建,對所有的邊(Edge)做字典序(Lexicographical Order)遍歷。
  • 通過使用后綴集合和最長公共前綴數組(LCP Array: Longest Common Prefix Array)來構建后綴樹,可在 O(n) 時間內完成,例如使用 Ukkonen 算法。構造后綴數組同樣也可以在 O(n) 時間內完成。
  • 每個通過后綴樹解決的問題,都可以通過組合使用后綴數組和額外的信息(例如:LCP Array)來解決。

 

后綴數組就是將文本串的所有后綴按照字典序進行排序,將后綴的起始字符的下標存入SA數組中。

比如字符串“ababa”, 他的后綴有ababa, baba, aba, ba, a

按照字典序排序為,a, aba, ababa, ba, baba他們的開始字符的下標分別為,5, 3, 1, 4, 2。因此SA[1~5]分別為5,3,1,4,2

 同時我們可以用rank來記后綴在所有后綴里的排名

構建SA[i]數組中相鄰元素的最長公共前綴(LCP,Longest Common Prefix),Height[i]表示SA[i]和SA[i-1]的LCP(i, j);H[i]=Height[Rank[i]表示Suffix[i]和字典排序在它前一名的后綴子串的LCP大小;
對於正整數i和j而言,最長公共前綴的定義如下: LCP(i, j) =lcp(Suffix(SA[i]), Suffix(SA[j])) = min(Height[k] | i + 1 <= k <= j)
也就是計算LCP(i, j)等同於查找Height數組中下標在i+1到j之間的元素最小值

 

暴力的構建后綴數組的方法復雜度是O(n^2logn),倍增算法(Doubling Algorithm)快速構造后綴數組,其利用了后綴子串之間的聯系可將時間復雜度降至O(MlogN),M為模式串的長度,N為目標串的長度;另外基數排序算法的時間復雜度為O(N);Difference Cover mod 3(DC3)算法(Linear Work Suffix Array Construction)可在O(3N)時間內構建后綴數組;Ukkonen算法(On-line Construction of Suffix-Trees)可在O(N)的時間內構建一棵后綴樹,然后再O(N)的時間內將后綴樹轉換為后綴數組,理論上最快的后綴數組構造法;

倍增法構造后綴數組,使用基數排序。基數排序在后綴數組中可以在O(n)的時間內對一個二元組(p,q)進行排序,其中p是第一關鍵字,q是第二關鍵字

我們把每個后綴分開來看。

開始時,每個后綴的第一個字母的大小是能確定的,也就是他本身的ASCLL值

具體點?把第ii個字母看做是(s[i],i)的二元組,對其進行基數排序

這樣我們就得到了他們的在完成第一個字母的排序之后的相對位置關系

sa[i]:排名為i的后綴的位置

rak[i]:從第i個位置開始的后綴的排名,下文為了敘述方便,把從第i個位置開始的后綴簡稱為后綴i

tp[i]:基數排序的第二關鍵字,意義與sa一樣

tax[i]i號元素出現了多少次。輔助基數排序

s[i]:字符串

“倍增法”,每次將排序長度*2,最多需要log(n)次便可以完成排序

因此我們現在需要對每個后綴的前兩個字母進行排序

此時第一個字母的相對關系我們已經知道了。

由於第i個后綴的第二個字母,實際是第i+1個后綴的第一個字母

因此每個后綴的第二個后綴的字母的相對位置關系我們也是知道的。

我們用tp這個數組把他記錄出來,對rak,tp這個二元組進行基數排序

接下來我們需要對每個后綴的前四個后綴進行排序

此時我們已經知道了每個后綴前兩個字母的排名,而第i個后綴的第3,4個字母恰好是第i+2個后綴的前兩個字母。

他們的相對位置我們又知道啦。

這樣不斷排下去,最后就可以完成排序啦

舉個栗子,banana先按第一個字母排序

然后給前兩個字母排序,也就是把相鄰二元組合並。再根據字典序排

再排前四個

 

最長公共前綴(LCP)

對兩個字符串u,v 定義函數lcp(u,v)=max{i|u=iv},對正整數i,j 定義LCP(i,j)=lcp(Suffix(SA[i]),Suffix(SA[j]),其中i,j 均為1 至n 的整數。

LCP(i,j)也就是后綴數組中第i 個和第j 個后綴的最長公共前綴的長度。

關於LCP 有兩個顯而易見的性質:

性質2.1 LCP(i,j)=LCP(j,i)

性質2.2 LCP(i,i)=len(Suffix(SA[i]))=n-SA[i]+1

這兩個性質的用處在於,我們計算LCP(i,j)時只需要考慮i<j 的情況,因為i>j時可交換i,j,i=j時可以直接輸出結果n-SA[i]+1。

並且LCP有一些定理【證明見OI2004國家集訓隊論文《后綴數組》許智磊,鏈接附在文后】

Lemma: 對任意1≤i<j<k≤n,LCP(i,k)=min{LCP(i,j),LCP(j,k)}

LCP Theorem: LCP(i,j)=min{LCP(k-1,k)|i+1≤k≤j} 

LCP Corollary 對 i≤j<k,LCP(j,k)≥LCP(i,k)

 

定義數組height,height[i] = LCP(i-1, i),那么根據定理可得LCP(i,j)=min{height[k]|i+1≤k≤j}

那么求LCP的問題就可以變成經典的RMQ問題【如果height是固定的】,可以用線段樹來維護。

【論文中提到的RMQ標准算法,O(n)時間預處理,O(1)完成查詢,太菜,不會。】

設i<n,j<n,Suffix(i)和Suffix(j)滿足lcp(Suffix(i),Suffix(j)>1,則成立以下兩點:

Fact 1 Suffix(i)<Suffix(j) 等價於Suffix(i+1)<Suffix(j+1)。

Fact 2 一定有lcp(Suffix(i+1),Suffix(j+1))=lcp(Suffix(i),Suffix(j))-1。

 

那么如何高效的求height數組?

為了描述方便,設h[i]=height[Rank[i]],即height[i]=h[SA[i]]。

h 數組滿足一個性質:對於i>1 且Rank[i]>1,一定有h[i]≥h[i-1]-1。

根據這個性質,可以令i從1 循環到n按照如下方法依次算出h[i]:

若 Rank[i]=1,則h[i]=0。字符比較次數為0。

若 i=1 或者h[i-1]≤1,則直接將Suffix(i)和Suffix(Rank[i]-1)從第一個字符開始依次比較直到有字符不相同,由此計算出h[i]。字符比較次數為h[i]+1,不超過h[i]-h[i-1]+2。

否則,說明i>1,Rank[i]>1,h[i-1]>1,根據性質3,Suffix(i)和Suffix(Rank[i]-1)至少有前h[i-1]-1 個字符是相同的,於是字符比較可以從h[i-1]開始,直到某個字符不相同,由此計算出h[i]。字符比較次數為h[i]-h[i-1]+2。

求出了h 數組,根據關系式height[i]=h[SA[i]]可以在O(n)時間內求出height數組,於是可以在O(n)時間內求出height 數組。

 

kuangbin的模板

 1 /*
 2 *suffix array
 3 *倍增算法  O(n*logn)
 4 *待排序數組長度為n,放在0~n-1中,在最后面補一個0
 5 *build_sa( ,n+1, );//注意是n+1;
 6 *getHeight(,n);
 7 *例如:
 8 *n   = 8;
 9 *num[]   = { 1, 1, 2, 1, 1, 1, 1, 2, $ };注意num最后一位為0,其他大於0
10 *rank[]  = { 4, 6, 8, 1, 2, 3, 5, 7, 0 };rank[0~n-1]為有效值,rank[n]必定為0無效值
11 *sa[]    = { 8, 3, 4, 5, 0, 6, 1, 7, 2 };sa[1~n]為有效值,sa[0]必定為n是無效值
12 *height[]= { 0, 0, 3, 2, 3, 1, 2, 0, 1 };height[2~n]為有效值
13 *
14 */
15 
16 int sa[MAXN];//SA數組,表示將S的n個后綴從小到大排序后把排好序的
17              //的后綴的開頭位置順次放入SA中
18 int t1[MAXN],t2[MAXN],c[MAXN];//求SA數組需要的中間變量,不需要賦值
19 int rank[MAXN],height[MAXN];
20 //待排序的字符串放在s數組中,從s[0]到s[n-1],長度為n,且最大值小於m,
21 //除s[n-1]外的所有s[i]都大於0,r[n-1]=0
22 //函數結束以后結果放在sa數組中
23 void build_sa(int s[],int n,int m)
24 {
25     int i,j,p,*x=t1,*y=t2;
26     //第一輪基數排序,如果s的最大值很大,可改為快速排序
27     for(i=0;i<m;i++)c[i]=0;
28     for(i=0;i<n;i++)c[x[i]=s[i]]++;
29     for(i=1;i<m;i++)c[i]+=c[i-1];
30     for(i=n-1;i>=0;i--)sa[--c[x[i]]]=i;
31     for(j=1;j<=n;j<<=1)
32     {
33         p=0;
34         //直接利用sa數組排序第二關鍵字
35         for(i=n-j;i<n;i++)y[p++]=i;//后面的j個數第二關鍵字為空的最小
36         for(i=0;i<n;i++)if(sa[i]>=j)y[p++]=sa[i]-j;
37         //這樣數組y保存的就是按照第二關鍵字排序的結果
38         //基數排序第一關鍵字
39         for(i=0;i<m;i++)c[i]=0;
40         for(i=0;i<n;i++)c[x[y[i]]]++;
41         for(i=1;i<m;i++)c[i]+=c[i-1];
42         for(i=n-1;i>=0;i--)sa[--c[x[y[i]]]]=y[i];
43         //根據sa和x數組計算新的x數組
44         swap(x,y);
45         p=1;x[sa[0]]=0;
46         for(i=1;i<n;i++)
47             x[sa[i]]=y[sa[i-1]]==y[sa[i]] && y[sa[i-1]+j]==y[sa[i]+j]?p-1:p++;
48         if(p>=n)break;
49         m=p;//下次基數排序的最大值
50     }
51 }
52 void getHeight(int s[],int n)
53 {
54     int i,j,k=0;
55     for(i=0;i<=n;i++)rank[sa[i]]=i;
56     for(i=0;i<n;i++)
57     {
58         if(k)k--;
59         j=sa[rank[i]-1];
60         while(s[i+k]==s[j+k])k++;
61         height[rank[i]]=k;
62     }
63 }

 

參考鏈接:

后綴樹 https://www.cnblogs.com/gaochundong/p/suffix_tree.html

后綴數組 https://www.cnblogs.com/gaochundong/p/suffix_array.html

https://www.douban.com/note/210945706/

https://www.cnblogs.com/jinkun113/p/4743694.html

OI2004國家集訓隊論文《后綴數組》許智磊https://github.com/Booooooooooo/OI-Public-Library/blob/master/%E5%9B%BD%E5%AE%B6%E9%9B%86%E8%AE%AD%E9%98%9F%E8%AE%BA%E6%96%871999-2017/2004/%E8%AE%B8%E6%99%BA%E7%A3%8A.pdf

 OI2009國家集訓隊論文《后綴數組——處理字符串的有力工具》https://github.com/Booooooooooo/OI-Public-Library/blob/master/%E5%9B%BD%E5%AE%B6%E9%9B%86%E8%AE%AD%E9%98%9F%E8%AE%BA%E6%96%871999-2017/2009/%E7%BD%97%E7%A9%97%E9%AA%9E/%E5%90%8E%E7%BC%80%E6%95%B0%E7%BB%84%E2%80%94%E2%80%94%E5%A4%84%E7%90%86%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E6%9C%89%E5%8A%9B%E5%B7%A5%E5%85%B7.pdf


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM