學習小記——後綴數組

定義

在字符串處理當中,後綴樹和後綴數組都是非常有力的工具,其中後綴樹大家瞭解得比較多,關於後綴數組則很少見於國內的資料。其實後綴數組是後綴樹的一個非常精巧的替代品,它比後綴樹容易編程實現,能夠實現後綴樹的很多功能而時間複雜度也不太遜色,並且,它比後綴樹所佔用的空間小很多。可以說,在信息學競賽中後綴數組比後綴樹要更爲實用。

變量定義

Suffix(i):表示S[i..len(S)],即從第i位開始的字符串後綴。

SA數組:後綴數組SA是一個一維數組,它保存1..n的某個排列SA[1],SA[2],……,SA[n],並保證Suffix(SA[i])<Suffix(SA[i+1]),1≤i<n。也就是將S的n個後綴從小到大進行排序之後把排好序的後綴的開頭位置順次放入SA中。

rank數組:名次數組 Rank[i] 保存的是 Suffix(i) 在所有後綴中從小到大排列的“ 名次 ”後綴數組SA與名次數組Rank的對應關係。簡單的說,後綴數組(SA)是“ 排第幾的是誰?”,名次數組(RANK)是“ 你排第幾?”。容易看出,後綴數組和名次數組爲互逆運算。

height數組:height[i]=LCP(i-1,i),即第rank[i-1]與rank[i]的最長前綴是多長。

2倍增算法

比較容易實現的一種(相對於DC3算法,雖然DC3比較快,但是實現有點複雜,我不會……),一般的題目應該也夠用了,時間複雜度O(n*log n),還算比較快了。

思路

我們倍增一個k,與RMQ的思想差不多,以2k1 的結果來推出2k ,一步步倍增就能夠實現求出所有的後綴的排名(具體定義詳見《後綴數組——處理字符串的有力工具(羅穗騫)》)。(借了個圖)
這裏寫圖片描述
假設現在到了第三輪,k爲2,第一關鍵字是上一輪同一位置的rank,第二關鍵字爲上一輪向後k個位置的rank,然後進行雙關鍵字排序(這裏推薦用基數排序,當然快排也行,但是慢一點)。

memset(t,0,sizeof(t));
fo(i,1,n) t[rank[i+k]]++;
fo(i,1,da) t[i]+=t[i-1];
for(i=n;i>0;i--) se[t[rank[i+k]]--]=i;
//第二關鍵字排序   
memset(t,0,sizeof(t));
fo(i,1,n) t[rank[i]]++;
fo(i,1,da) t[i]+=t[i-1];
for(i=n;i>0;i--) sa[t[rank[se[i]]]--]=se[i];
//第一關鍵字排序

就這樣可以求出一個SA數組,但是數組之中都是不重複的,我們還要處理一下相同的情況。當S[i]=S[j]&&S[i+k]=S[j+k]時就定義爲相同。

bool cmp(int *a,int x,int y,int z){return a[x]==a[y]&&a[x+z]==a[y+z];}

j=1;zs[sa[1]]=1;
        fo(i,2,n) 
            zs[sa[i]]=cmp(rank,sa[i],sa[i-1],k)?j:++j;

求完了rank和SA數組後,就要求height數組,通過這個數組的一個特殊性質:h[i]≥h[i-1]-1,我們可以把複雜度降至O(n)

i=j=k=0;
    for(int i=1;i<=n;h[rank[i++]]=k)
        for(k?k--:0,j=sa[rank[i]-1];a[i+k]==a[j+k];++k);

模板題

文件修復(JZOJ 1598)

//快速排序
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<math.h>
using namespace std;
#define fo(i,a,b) for(i=a;i<=b;i++)
const int N=1e5+5;
struct arr{
    int x,y,bh;
}t[N];
char s[N];
int n,i,j,k,rank[N],sa[N],ws[N],a[N],h[N],num,ans;
int cmp(arr x,arr y){return x.x<y.x||(x.x==y.x&&x.y<y.y);}
void deal(){
    fo(i,1,n) rank[i]=a[i];
    for(k=0;k<=n;){
        fo(i,1,n){
            t[i].x=rank[i],t[i].bh=i;
            if(i+k>n) t[i].y=0;else t[i].y=rank[i+k];
        }
        sort(t+1,t+n+1,cmp);
        rank[t[1].bh]=1;sa[1]=t[1].bh;num=1;
        fo(i,2,n){
            if(t[i-1].x!=t[i].x||t[i-1].y!=t[i].y) num++;
            rank[t[i].bh]=num;
            sa[i]=t[i].bh;
        }
        k*=2;
        if(k==0) k=1;
    }
    i=j=k=0;
    for(int i=1;i<=n;h[rank[i++]]=k)
        for(k?k--:0,j=sa[rank[i]-1];a[i+k]==a[j+k];++k);
}
int main(){
    scanf("%s",s);
    n=strlen(s);
    fo(i,1,n) a[i]=s[i-1];
    deal();
    ans=h[2];
    fo(i,3,n) ans+=max(0,h[i]-h[i-1]);
    printf("%d",ans);
}

還有一種快一點

//基數排序
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<math.h>
using namespace std;
#define fo(i,a,b) for(i=a;i<=b;i++)
const int N=1e5+5;
char s[N];
int n,i,j,k,rank[N],sa[N],zs[N],se[N],t[N],a[N],h[N],num,ans,da;
bool cmp(int *a,int x,int y,int z){return a[x]==a[y]&&a[x+z]==a[y+z];}
void deal(){
    fo(i,1,n) rank[i]=a[i],se[i]=i;
    for(k=0;k<=n;){
        memset(t,0,sizeof(t));
        fo(i,1,n) t[rank[i+k]]++;
        fo(i,1,da) t[i]+=t[i-1];
        for(i=n;i>0;i--) se[t[rank[i+k]]--]=i;

        memset(t,0,sizeof(t));
        fo(i,1,n) t[rank[i]]++;
        fo(i,1,da) t[i]+=t[i-1];
        for(i=n;i>0;i--) sa[t[rank[se[i]]]--]=se[i];
        j=1;zs[sa[1]]=1;
        fo(i,2,n) 
            zs[sa[i]]=cmp(rank,sa[i],sa[i-1],k)?j:++j;
        swap(zs,rank);
        if(k==0) k=1;else k*=2;
    }
    i=j=k=0;
    for(int i=1;i<=n;h[rank[i++]]=k)
        for(k?k--:0,j=sa[rank[i]-1];a[i+k]==a[j+k];++k);
}
int main(){
    scanf("%s",s);
    n=strlen(s);
    fo(i,1,n) a[i]=s[i-1],da=max(da,a[i]);
    da=max(n,1000);
    deal();
    ans=h[2];
    fo(i,3,n) ans+=max(0,h[i]-h[i-1]);
    printf("%d",ans);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章