字尾陣列學習筆記,待續
看了好久,,,簡單擠一擠
例題:讀入一個長度為n的由大小寫英文字母或數字組成的字串,請把這個字串的所有非空字尾按字典序從小到大排序,然後按順序輸出字尾的第一個字元在原串中的位置。位置編號為1到n。
先看一下雜湊加二分。。注意細節。。只有70分。。因為複雜度(n*log²n),1e6要T。
#include<bits/stdc++.h> #define ull unsigned long long using namespace std; const int maxn=1e6+10; const ull base=131; char a[maxn]; ull Hash[maxn],power[maxn]; int n,sa[maxn]; inline void Hash_work(){ Hash[n+1]=0,power[0]=1; for(int i=n;i>=1;--i){ sa[i]=i,power[n-i+1]=power[n-i]*base; Hash[i]=Hash[i+1]*base+a[i]; } } inline ull Get_Hash(int l,int r){return Hash[l]-power[r-l+1]*Hash[r+1];} inline bool cmp(int L,int R){ int l=0,r=min(n-L+1,n-R+1); //二分lcp的長度 while(l<r){ int mid=(l+r+1)>>1; //看前mid個是否相同 if(Get_Hash(L,L+mid-1)==Get_Hash(R,R+mid-1)) l=mid; else r=mid-1; } return a[L+l]<a[R+l]; } inline void print(int x){ if(x>9) print(x/10); putchar(x%10+'0'); } int main(){ scanf("%s",a+1),n=strlen(a+1),Hash_work(); sort(sa+1,sa+n+1,cmp); for(int i=1;i<=n;++i) print(sa[i]),putchar(' '); }
字尾陣列
後面要出現的陣列含義如下:
suf[i]:假設原串為S[1~n],那麼suf[i]就代表S[i~n],也就是從i開始的字尾。
sa[i]:排名為i的字尾的位置。
rank[i]:位置為i的字尾的排名。
height[i]:suf[sa[i]]和suf[sa[i-1]]的最長公共字首的長度。(注意,height陣列的下標代表排名!)
關於如何求sa[i],rank[i],有個方法叫倍增,這裡倍增字串的長度。
假設原串長度為n。我們從長度為1開始倍增。最開始,我們有n個長度為1的字串。
然後比較出它們的大小(sort),然後我們得到了它們的大小關係。
然後考慮長度為2。對於最後一個,在它後面補一個0就行了。類似的,後面長度不夠的都補0,不會影響大小的比較。
現在我們有n個長度為2的字串。
那麼這個時候比較兩個子串的大小就可以這樣:把兩個字串平都分成兩份。先看這兩個字串的前一半,這兩個一半的長度為1,那麼它們的大小關係是已知的。如果現在比較出了這兩個一半的大小,那麼也就比出了這兩個長度為2的子串的大小。如果它們的前一半相同,那麼就看它們的後一半。
以此類推。
結合圖解釋一下:a和b是原串的兩個子串。它們的長度相同。a1,a2是a的前一半和後一半,b1,b2是b的前一半和後一半。
若a1>b1,則a>b。
若a1=b1:若a2<b2,則a<b。若a2=b2,則a=b。若a2>b2,則a>b。
若a1<b1,則a<b。
這個時候一次比較就是O(1)的。
比較出長度為2^k(k∈[0,logn])的所有子串的大小需要O(n logn),一共要比較log n次,那麼總的複雜度就是O(n log²n)。
可以用基數排序再優化優化。
每次比較需要兩個關鍵字。可以看做是個兩位數。仿照基數排序的思想。
先把所有數丟到個位桶裡。舉個例子:
比如有幾個數:61,35,11,21,15,23。
(這裡的遍歷都假設為:先進去的先遍歷,後進去的後遍歷)
現在有10個桶,分別為0~9,代表個位為0~9。我們把這些數丟進去之後再把每個桶(按個位從小到大)遍歷一下:
個位為1:{61,11,21} 個位為3:{23} 個位為5:{35,15} (空的桶懶得寫了)
遍歷一遍:61,11,21,23,35,15
我們發現,這些數遍歷完後,它們的個位一定是從小到大排的。
然後我們把它們丟到十位桶裡。
丟進十位桶裡的順序按照剛才遍歷出來的順序:61,11,21,23,35,15
十位為1:{11,15} 十位為2:{21,23} 十位為3:{35} 十位為6:{61} 然後就發現它們有序了。為什麼呢?
對於一個單獨的十位的桶,它們的十位都是一樣的。它們的大小比較只看個位。然而我們之前把這些數丟進去的時候是保證了整個序列的個位是從小到大排的。那麼先丟進去的數的個位就小,後丟進去的數的個位就大。那麼對於任意一個桶,我們去遍歷它的時候,就可以保證先遍歷到的數的個位就小,後遍歷到的數的個位就大。那麼對於這個桶,遍歷完之後,得到的序列就是從小到大排列的。
然後十位從1~9遍歷,由於每個桶遍歷完都是從小到大的,那麼得到的總序列就肯定是從小到大排的了。
然而這裡的兩位數代表的是一個二元組,它的兩個位置不一定是0~9。但是總而言之,思想是一樣的。
貼個板子。抄過來的。參考部落格裡解釋很詳細。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
char s[maxn];
int n,m=200,num;
int cnt[maxn],x[maxn],y[maxn],sa[maxn];
inline void print(int x){
if(x>9) print(x/10);
putchar(x%10+'0');
}
inline void Get_Sa(){
for(int i=1;i<=n;++i) ++cnt[x[i]=s[i]];
for(int i=2;i<=m;++i) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[x[i]]--]=i;
for(int k=1;k<=n;k<<=1){
int num=0;
for(int i=n-k+1;i<=n;++i) y[++num]=i;
for(int i=1;i<=n;++i) if(sa[i]>k) y[++num]=sa[i]-k;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;++i) ++cnt[x[i]];
for(int i=2;i<=m;++i) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;--i) sa[cnt[x[y[i]]]--]=y[i],y[i]=0;
swap(x,y),x[sa[1]]=1,num=1;
for(int 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);
if(num==n) break;
m=num;
}
for(int i=1;i<=n;++i) print(sa[i]),putchar(' ');
}
int main(){
scanf("%s",s+1);
n=strlen(s+1);
Get_Sa();
}