轉載自後綴數組 學習筆記html
後綴數組這個東西真的是神仙操做……ios
可是這個比較神仙的東西在網上的講解通常都僅限於思想而不是代碼,並且這個東西開一堆數組,不少初學者寫代碼的時候很容易發生歧義理解,因此這裏給出一個比較詳細的講解。筆者本身也是和後綴數組硬剛了一個上午外加一箇中午才理解的板子。git
本人版權意識薄弱,若有侵權現象請聯繫博主郵箱xmzl200201@126.comgithub
參考文獻:數組
如下是不認識的dalao們:優化
wsy的cnblogspa
soda的cnblog.net
特別感謝如下的兩位dalao,寫的特別好,打call
咱們先看幾條定義:
在字符串s中,取任意i<=j,那麼在s中截取從i到j的這一段就叫作s的一個子串
後綴就是從字符串的某個位置i到字符串末尾的子串,咱們定義以s的第i個字符爲第一個元素的後綴爲suff(i)
把s的每一個後綴按照字典序排序,
後綴數組sa[i]就表示排名爲i的後綴的起始位置的下標
而它的映射數組rk[i]就表示起始位置的下標爲i的後綴的排名
簡單來講,sa表示排名爲i的是啥,rk表示第i個的排名是啥
必定要記牢這些數組的意思,後面看代碼的時候若是記不牢的話就絕對看不懂
先說最暴力的狀況,快排(n log n)每一個後綴,可是這是字符串,因此比較任意兩個後綴的複雜度實際上是O(n),這樣一來就是接近O(n^2 log n)的複雜度,數據大了確定是不行的,因此咱們這裏有兩個優化。
ps:本文中的^表示平方而不是異或
首先讀入字符串以後咱們現根據單個字符排序,固然也能夠理解爲先按照每一個後綴的第一個字符排序。對於每一個字符,咱們按照字典序給一個排名(固然能夠並列),這裏稱做關鍵字。
接下來咱們再把相鄰的兩個關鍵字合併到一塊兒,就至關於根據每個後綴的前兩個字符進行排序。想一想看,這樣就是以第一個字符(也就是本身自己)的排名爲第一關鍵字,以第二個字符的排名爲第二關鍵字,把組成的新數排完序以後再次標號。沒有第二關鍵字的補零。
既然是倍增,就要有點倍增的樣子。接下來咱們對於一個在第i位上的關鍵字,它的第二關鍵字就是第(i+2)位置上的,聯想一下,由於如今第i位上的關鍵字是suff(i)的前兩個字符的排名,第i+2位置上的關鍵字是suff(i+2)的前兩個字符的排名,這兩個一合併,不就是suff(i)的前四個字符的排名嗎?方法同上,排序以後從新標號,沒有第二關鍵字的補零。同理咱們能夠證實,下一次咱們要合併的是第i位和第i+4位,以此類推便可……
ps:本文中的「第i位」表示下標而不是排名。排名的話我會說「排名爲i」
那麼咱們何時結束呢?很簡單,當全部的排名都不一樣的時候咱們直接退出就能夠了,由於已經排好了。
顯然這樣排序的速度穩定在(log n)
若是咱們用快排的話,複雜度就是(n log^2 n) 仍是太大。
這裏咱們用一波基數排序優化一下。在這裏咱們能夠注意到,每一次排序都是排兩位數,因此基數排序能夠將它優化到O(n)級別,總複雜度就是(n log n)。
介紹一下什麼是基數排序,這裏就拿兩位數舉例
咱們要建兩個桶,一個裝個位,一個裝十位,咱們先把數加到個位桶裏面,再加到十位桶裏面,這樣就能保證對於每一個十位桶,桶內的順序確定是按個位升序的,很好理解。
話說這個費了我好長時間,就爲了證實幾條定理……懶得證實的話背過就好了,不過筆者仍是以爲知道證實用起來更踏實一些,話說個人證實過程應該比較好懂,適合初學者理解……
咱們定義LCP(i,j)爲suff(sa[i])與suff(sa[j])的最長公共前綴
後綴數組這個東西,不可能只讓你排個序就完事了……大多數狀況下咱們都須要用到這個輔助工具LCP來作題的
這兩條性質有什麼用呢?對於i>j的狀況,咱們能夠把它轉化成i<j,對於i==j的狀況,咱們能夠直接算長度,因此咱們直接討論i<j的狀況就能夠了。
咱們每次依次比較字符確定是不行的,單次複雜度爲O(n),過高了,因此咱們要作必定的預處理才行。
LCP(i,k)=min(LCP(i,j),LCP(j,k)) 對於任意1<=i<=j<=k<=n
證實:設p=min{LCP(i,j),LCP(j,k)},則有LCP(i,j)≥p,LCP(j,k)≥p。
設suff(sa[i])=u,suff(sa[j])=v,suff(sa[k])=w;
因此u和v的前p個字符相等,v和w的前p個字符相等
因此u和w的前p的字符相等,LCP(i,k)>=p
設LCP(i,k)=q>p 那麼q>=p+1
由於p=min{LCP(i,j),LCP(j,k)},因此u[p+1]!=v[p+1] 或者 v[p+1]!=w[p+1]
可是u[p+1]=w[p+1] 這不就自相矛盾了嗎
因此LCP(i,k)<=p
綜上所述LCP(i,k)=p=min{LCP(i,j),LCP(j,k)}
LCP(i,k)=min(LCP(j,j-1)) 對於任意1<i<=j<=k<=n
這個結合LCP Lemma就很好理解了
咱們能夠把i~k拆成兩部分i~(i+1)以及(i+1)~k
那麼LCP(i,k)=min(LCP(i,i+1),LCP(i+1,k))
咱們能夠把(i+1)~k再拆,這樣就像一個DP,正確性顯然
咱們設height[i]爲LCP(i,i-1),1<i<=n,顯然height[1]=0;
由LCP Theorem可得,LCP(i,k)=min(height[j]) i+1<=j<=k
那麼height怎麼求,枚舉嗎?NONONO,咱們要利用這些後綴之間的聯繫
設h[i]=height[rk[i]],一樣的,height[i]=h[sa[i]];
那麼如今來證實最關鍵的一條定理:
h[i]>=h[i-1]-1;
證實過程來自曲神學長的blog,我作了一點改動方便初學者理解:
首先咱們不妨設第i-1個字符串按排名來的前面的那個字符串是第k個字符串,注意k不必定是i-2,由於第k個字符串是按字典序排名來的i-1前面那個,並非指在原字符串中位置在i-1前面的那個第i-2個字符串。
這時,依據height[]的定義,第k個字符串和第i-1個字符串的公共前綴天然是height[rk[i-1]],如今先討論一下第k+1個字符串和第i個字符串的關係。
第一種狀況,第k個字符串和第i-1個字符串的首字符不一樣,那麼第k+1個字符串的排名既可能在i的前面,也可能在i的後面,但沒有關係,由於height[rk[i-1]]就是0了呀,那麼不管height[rk[i]]是多少都會有height[rk[i]]>=height[rk[i-1]]-1,也就是h[i]>=h[i-1]-1。
第二種狀況,第k個字符串和第i-1個字符串的首字符相同,那麼因爲第k+1個字符串就是第k個字符串去掉首字符獲得的,第i個字符串也是第i-1個字符串去掉首字符獲得的,那麼顯然第k+1個字符串要排在第i個字符串前面。同時,第k個字符串和第i-1個字符串的最長公共前綴是height[rk[i-1]],
那麼天然第k+1個字符串和第i個字符串的最長公共前綴就是height[rk[i-1]]-1。
到此爲止,第二種狀況的證實尚未完,咱們能夠試想一下,對於比第i個字符串的排名更靠前的那些字符串,誰和第i個字符串的類似度最高(這裏說的類似度是指最長公共前綴的長度)?顯然是排名緊鄰第i個字符串的那個字符串了呀,即sa[rank[i]-1]。可是咱們前面求得,有一個排在i前面的字符串k+1,LCP(rk[i],rk[k+1])=height[rk[i-1]]-1;
又由於height[rk[i]]=LCP(i,i-1)>=LCP(i,k+1)
因此height[rk[i]]>=height[rk[i-1]]-1,也即h[i]>=h[i-1]-1。
注意上面那個題不用求lcp……看代碼建議先大略掃一遍,由於的確有點繞
#include<iostream> #include<cstdio> #include<cstring> #define rint register int #define inv inline void #define ini inline int #define maxn 1000050 using namespace std; char s[maxn]; int y[maxn],x[maxn],c[maxn],sa[maxn],rk[maxn],height[maxn],wt[30]; int n,m; inv putout(int x) { if(!x) { putchar(48); return; } rint l=0; while(x) wt[++l]=x%10,x/=10; while(l) putchar(wt[l--]+48); } inv get_SA() { for (rint i=1; i<=n; ++i) ++c[x[i]=s[i]]; //c數組是桶 //x[i]是第i個元素的第一關鍵字 for (rint i=2; i<=m; ++i) c[i]+=c[i-1]; //作c的前綴和,咱們就能夠得出每一個關鍵字最可能是在第幾名 for (rint i=n; i>=1; --i) sa[c[x[i]]--]=i; for (rint k=1; k<=n; k<<=1) { rint num=0; for (rint i=n-k+1; i<=n; ++i) y[++num]=i; //y[i]表示第二關鍵字排名爲i的數,第一關鍵字的位置 //第n-k+1到第n位是沒有第二關鍵字的 因此排名在最前面 for (rint i=1; i<=n; ++i) if (sa[i]>k) y[++num]=sa[i]-k; //排名爲i的數 在數組中是否在第k位之後 //若是知足(sa[i]>k) 那麼它能夠做爲別人的第二關鍵字,就把它的第一關鍵字的位置添加進y就好了 //因此i枚舉的是第二關鍵字的排名,第二關鍵字靠前的先入隊 for (rint i=1; i<=m; ++i) c[i]=0; //初始化c桶 for (rint i=1; i<=n; ++i) ++c[x[i]]; //由於上一次循環已經算出了此次的第一關鍵字 因此直接加就好了 for (rint i=2; i<=m; ++i) c[i]+=c[i-1]; //第一關鍵字排名爲1~i的數有多少個 for (rint i=n; i>=1; --i) sa[c[x[y[i]]]--]=y[i],y[i]=0; //由於y的順序是按照第二關鍵字的順序來排的 //第二關鍵字靠後的,在同一個第一關鍵字桶中排名越靠後 //基數排序 swap(x,y); //這裏不用想太多,由於要生成新的x時要用到舊的,就把舊的複製下來,沒別的意思 x[sa[1]]=1; num=1; for (rint i=2; i<=n; ++i) x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) ? num : ++num; //由於sa[i]已經排好序了,因此能夠按排名枚舉,生成下一次的第一關鍵字 if (num==n) break; m=num; //這裏就不用那個122了,由於都有新的編號了 } for (rint i=1; i<=n; ++i) putout(sa[i]),putchar(' '); } inv get_height() { rint k=0; for (rint i=1; i<=n; ++i) rk[sa[i]]=i; for (rint i=1; i<=n; ++i) { if (rk[i]==1) continue;//第一名height爲0 if (k) --k;//h[i]>=h[i-1]-1; rint j=sa[rk[i]-1]; while (j+k<=n && i+k<=n && s[i+k]==s[j+k]) ++k; height[rk[i]]=k;//h[i]=height[rk[i]]; } putchar(10); for (rint i=1; i<=n; ++i) putout(height[i]),putchar(' '); } int main() { gets(s+1); n=strlen(s+1); m=122; //由於這個題不讀入n和m因此要本身設 //n表示原字符串長度,m表示字符個數,ascll('z')=122 //咱們第一次讀入字符直接不用轉化,按原來的ascll碼來就能夠了 //由於轉化數字和大小寫字母還得分類討論,怪麻煩的 get_SA(); //get_height(); }