後綴數組模板及應用小結 附加練習題*6
後綴數組是後綴Trie的一個替代品。一個字符串的後綴Trie是把這個字符串所有的後綴給插入到一個Trie中。由於字符串的任意一個子串一定是這個字符串某個後綴的前綴,所以說可以直接在這個Trie裏面進行查找就可以找到任意一個字符串是否在這個字符串中,但是最壞情況下這棵Trie的空間復雜度(或者說結點數)可以到達O(N^2)級別,因此需要優化,於是誕生了後綴樹,而後綴數組便是後綴樹的一個簡單替代品。現在在這個字符串的後面加入一個字符$,並規定這個字符小於串中的所有其它字符,想象一下把一棵Trie的每個結點的所有兒子從小到大排序,那麽不難發現所有的$都和這棵Trie的一個葉子一一對應。後綴數組(sa)儲存的是Trie從左到右所有的的葉子在原串中對應的後綴的首字符的數組下標(規定排序的時候小的結點在左邊,大的在右邊)。
後綴數組本身就大致介紹完了,先貼出後綴數組的O(NlogN)構造算法:(感謝某寫的書中代碼很實用但是有時候充滿了bug的Lrj)
(關於此算法以及其中用到的各種奇奇妙妙的技巧不做介紹,這只是小結)
1 int sa[maxn],c[maxn],t1[maxn<<1],t2[maxn<<1]; 2 void build_sa(int m) 3 { 4 int i,*x=t1,*y=t2; 5 for(i=0;i<m;i++) c[i]=0; 6 for(i=0;i<n;i++) c[x[i]=S[i]]++; 7 for(i=1;i<m;i++) c[i]+=c[i-1]; 8 for(i=n-1;i>=0;i--) sa[--c[x[i]]]=i; 9 for(int k=1;k<=n;k<<=1){ 10 int p=0; 11 for(i=n-k;i<n;i++) y[p++]=i; 12 for(i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k; 13 for(i=0;i<m;i++) c[i]=0; 14 for(i=0;i<n;i++) c[x[i]]++;15 for(i=1;i<m;i++) c[i]+=c[i-1]; 16 for(i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i]; 17 swap(x,y); 18 p=1,x[sa[0]]=0; 19 for(i=1;i<n;i++) x[sa[i]]=y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+k]==y[sa[i]+k]?p-1:p++; 20 if(p==n) break; 21 m=p; 22 } 23 }
還有兩個非常重要的東西——rank和height數組,rank[i]表示的是以原字符串i位置開頭的後綴在sa中的數組下標,height[i]表示的是後綴sa[i-1]和後綴sa[i]的LCP(最長公共前綴),height的構造算法用到了遞推來以復雜度O(N)的時間復雜度完成。具體證明請見lrj的大白書(ORZ實際上是我時間不多而且我比較懶)
給出height數組的構造代碼:
1 void getHeight() 2 { 3 for(int i=0;i<n;i++) rank[sa[i]]=i; 4 int k=0; 5 for(int i=0;i<n;i++){ 6 if(!rank[i]){ height[0]=k=0; continue; } 7 if(k) k--; 8 int j=sa[rank[i]-1]; 9 while(S[j+k]==S[i+k]) k++; 10 height[rank[i]]=k; 11 } 12 }
於是我們就可以做很多事情辣!!!
Q1:計算任意兩個後綴的最長公共前綴。
當你理解height數組實際上是後綴Trie上葉子i和葉子i-1的LCA的深度(規定後綴Trie的深度為0)之後,不難發現當你詢問後綴x,y的最長公共前綴的時候你只需要在height中進行一次RMQ(rank[x]+1,rank[y])就可以了(假定rank[x]+1<=rank[y]),道理可以參考dfs序下的用dep數組求LCA的算法,或者用Trie來形象理解。
RMQ是靜態的,用st表實現。給出st表的構造和查詢以及解決問題的代碼:
1 void getst() 2 { 3 for(int i=0;i<n;i++) d[i][0]=height[i]; 4 for(int i=n-1;i>=0;i--) 5 for(int j=1;(1<<j)<=n-i;j++) 6 d[i][j]=min(d[i][j-1],d[i+(1<<j-1)][j-1]); 7 } 8 int RMQ(int x,int y) 9 { 10 int k=0; 11 while((1<<k+1)<y-x+1) k++; 12 return min(d[x][k],d[y-(1<<k)+1][k]); 13 } 14 void work() 15 { 16 scanf("%d",&Q); 17 for(int i=1;i<=Q;i++){ 18 scanf("%d%d",&x,&y); 19 x=rank[x],y=rank[y]; 20 if(x>y) swap(x,y); 21 if(x!=y) printf("%d\n",RMQ(x+1,y));; 22 else printf("%d\n",x+1); 23 } 24 }
Q2:計算最長的可重疊重復子串和最長的不可重疊重復子串。
可重疊的最長重復子串你發現就是取height的最大值。不可重疊的采用二分,每一次二分一個長度mid,相當於所有height[i]<mid的都變成一塊隔板,把sa數組變成了幾段,求每一段裏面sa[i]的最大值MAX和最小值MIN,如果存在一組MAX-MIN>=mid就說明mid是可行解,二分的區間左端點右移,否則二分的區間右端點左移(思考這個問題請註意height的意義)。
後一問代碼:
1 void work() 2 { 3 int ans=0,A=0,B=n+1,mid; 4 while(A<B){ 5 mid=A+B>>1; bool ok=0; 6 int MAX=sa[0],MIN=sa[0]; 7 for(int i=1;i<n;i++){ 8 if(height[i]>=mid){ 9 MAX=max(MAX,sa[i]),MIN=min(MIN,sa[i]); 10 if(MAX-MIN>=mid) { ok=1; break; } 11 } 12 else MAX=MIN=sa[i]; 13 } 14 if(ok) A=mid+1,ans=mid; 15 else B=mid; 16 } 17 printf("%d\n",ans); 18 }
Q3:計算不同子串個數(。。。實際上就是看後綴Trie上有多少個結點,代碼就不給了。)。
上面的問題全部都是關於某個字符串後綴的,怎麽靈活應用呢?答案是:當你要把一堆串進行處理的時候,把它們接起來,每個字符串的後面丟一個分隔符(互不相同,沒有在原字符串中出現過),比如下面這三個問題:
Q4:計算這兩個字符串的最長公共子串,並輸出字典序最小的那個。
對於給出的兩個字符串(假設全部都是小寫字母),將它們接起來,中間插入一個A,你可以發現任意跨越A位置的兩個後綴的LCP正是這兩個後綴的最長相同前綴(就是這兩個位置開頭的最長相同子串)。因為這兩個後綴的LCP一定不包含A。可以發現當RMQ查詢區間一段區間的時候,左端點不動,右端點右移,結果具有單調遞減的性質。所以只需要掃一遍sa判斷是不是這個和上一個對應的位置恰好在分隔符的兩端,是的話對height取max就可以了。輸出的時候再走一遍即可(利用sa的字典序性質)。
1 void work() 2 { 3 int ans=0; 4 for(int i=1;i<n;i++) 5 if(sa[i-1]<nn&&sa[i]>nn||sa[i-1]>nn&&sa[i]<nn) ans=max(ans,height[i]); 6 printf("%d\n",ans); 7 for(int i=1;i<n;i++) if((sa[i-1]<nn&&sa[i]>nn||sa[i-1]>nn&&sa[i]<nn)&&height[i]==ans1){ 8 for(int j=0;j<height[i];j++) tt[j]=S[sa[i]+j]; 9 break; 10 } 11 puts(tt); 12 }
Q5:計算長度不小於K的公共子串的個數(子串相同但位置不同算不同的子串)。
例如:T="xx",P="xx",K=1,則長度不小於K 的公共子串的個數是5。
再如:T="aababaa",P="abaabaa",K=2,則長度不小於K 的公共子串的個數是22。
可以發現這個問題是有單調性做法的。定義兩個點(字符串兩個下標)的種類不同當且僅當這兩個點位於分隔點的兩端。單獨看兩個點x,y的貢獻就是LCP(x,y)-K+1。基本的思路是一路上計算每個點和前面的異種類點對答案產生的貢獻。令cnt1表示當前遇到的分隔點前的點的數量,cnt2表示當前遇到的分隔點後的點的數量。可以發現當height一路單調遞減的時候可以直接一路用當前的height來計算當前點和前面的異種類點對答案產生的共修貢獻,然後更新cnt1和cnt2。但是有個問題,如果出現了height從某一段開始遞減的情況怎麽破?借助前面的思考,發現可以用單調棧來實現,當height[i]大於等於棧頂的時候直接丟到棧裏面去,否則按照出棧的順序依次合並棧頂元素的信息,計算貢獻(相當於sa序列反著看原來遞增的height數組變成了遞減的),當height[i]<K的時候全部出棧,合並信息,然後把這個點丟進去墊底。(加入的分割符ascll碼為1,好像終端裏面打出來是個笑臉)
1 struct data{ int v,cnt1,cnt2; }stk[maxn]; int top; 2 void work() 3 { 4 long long ans=0; 5 for(int i=1;i<=n;i++){ 6 if(height[i]<K){ 7 while(top>1){ 8 ans+=(1ll*stk[top].cnt1*stk[top-1].cnt2+1ll*stk[top].cnt2*stk[top-1].cnt1)*(stk[top].v-K+1); 9 stk[top-1].cnt1+=stk[top].cnt1,stk[top-1].cnt2+=stk[top].cnt2; 10 top--; 11 } 12 stk[top=1]=(data){0,sa[i]<nn,sa[i]>nn}; 13 } 14 else{ 15 if(stk[top].v<=height[i]) stk[++top]=(data){height[i],sa[i]<nn,sa[i]>nn}; 16 else{ 17 while(stk[top].v>height[i]){ 18 ans+=(1ll*stk[top].cnt1*stk[top-1].cnt2+1ll*stk[top].cnt2*stk[top-1].cnt1)*(stk[top].v-K+1); 19 stk[top-1].cnt1+=stk[top].cnt1,stk[top-1].cnt2+=stk[top].cnt2; 20 top--; 21 } 22 stk[++top]=(data){height[i],sa[i]<nn,sa[i]>nn}; 23 } 24 } 25 } 26 cout<<ans<<‘\n‘; 27 }
Q6:輸入N個小寫字符串,你的任務是求出一個長度最大的字符串,使得它在超過一半的給出字符串中作為子串出現。如果有多解,按照字典序從小到大輸出所有解,N<=100,每個字符串長度<=1000。(來自某大白書:the form of life!)
還是有單調做法。一樣的思路,先用相同的分隔符隔開然後求一遍sa和height,原本是要讓所有的分隔符互不相同才能保證正確性的,分隔符相同sa數組性質並沒有太大實際影響,關鍵是height數組就有可能不對頭了,所以需要手動調整一下height,記錄len[i]表示第i個字符串加入之後當前整個字符串的長度(包括分隔符),belong[i]表示某個位置的字符屬於哪個字符串(對於分隔符來說belong=0),於是可以通過計算sa[i]和sa[i-1]在原字符串中作為後綴的長度來和height[i]取min調整。之後的大致思路:當前[l,r]的最小值小於等於答案一定不會更優,當滿足出現次數的限制的時候應該讓l跳到[l,r]最小值最後一次出現的位置,查詢兩個後綴LCP長度的時候只有區間內最小的height起作用,利用這些不難腦補出一個利用單調隊列完成的線性算法(主要利用對height查詢的單調性)。
判掉N=1的情況後,註意一下細節就好。(原題多組數據)
1 void data_in() 2 { 3 n=0; 4 memset(S,0,sizeof(S)); 5 for(int i=1;i<=N;i++){ 6 gets(S+n); 7 len[i]=strlen(S),belong[len[i]]=0; 8 for(int j=n;j<strlen(S);j++) belong[j]=i; 9 n=strlen(S)+1,S[n-1]=‘A‘; 10 } 11 } 12 void work() 13 { 14 if(N==1){ 15 S[n-1]=‘\0‘; puts(S); putchar(‘\n‘); 16 return; 17 } 18 build_sa(200); 19 getHeight(); 20 for(int i=1;i<n;i++){ 21 int v1=max(0,len[belong[sa[i]]]-sa[i]),v2=max(0,len[belong[sa[i-1]]]-sa[i-1]); 22 height[i]=min(height[i],min(v1,v2)); 23 } 24 int l=0,r=0,cnt=0,ans=0; 25 memset(vis,0,sizeof(vis)); 26 while(r<n){ 27 if(height[r]<=ans){ 28 while(l<r) vis[belong[sa[l++]]]=0; 29 cnt=0; 30 if(belong[sa[r]]) vis[belong[sa[r]]]++,cnt++; 31 } 32 else{ 33 while(front!=rear&&mq[rear-1].v>height[r]) rear--; 34 if(front!=rear&&mq[rear-1].v==height[r]) mq[rear-1].pos=r; 35 else mq[rear++]=(data){r,height[r]}; 36 if(belong[sa[r]]&&vis[belong[sa[r]]]++==0) cnt++; 37 while(cnt*2>N){ 38 ans=mq[front].v; 39 while(l<mq[front].pos){ 40 if(belong[sa[r]]&&--vis[belong[sa[l]]]==0) cnt--; 41 l++; 42 } 43 front++; 44 } 45 } 46 r++; 47 } 48 if(!ans) { puts("?"); putchar(‘\n‘); return; } 49 for(int i=1;i<n;i++) if(height[i]>=ans){ 50 memset(vis,0,sizeof(vis)); cnt=0; 51 if(belong[sa[i-1]]&&vis[belong[sa[i-1]]]++==0) cnt++; 52 if(vis[belong[sa[i]]]++==0) cnt++; 53 while(i!=n-1&&height[i+1]>=ans) 54 if(vis[belong[sa[++i]]]++==0) cnt++; 55 if(cnt*2<=N) continue; 56 for(int j=sa[i];j<sa[i]+ans;j++) putchar(S[j]); 57 putchar(‘\n‘); 58 } 59 putchar(‘\n‘); 60 }
後綴數組模板及應用小結 附加練習題*6