1. 程式人生 > >字尾陣列與基數排序

字尾陣列與基數排序

字串裡算難了。。參考了好多部落格

推薦https://blog.csdn.net/YxuanwKeith/article/details/50636898

 

每個陣列的含義:sa[i]:排在第i位的子串的位置,、

        rank[i]:位置 i 的子串排在第幾位

        height[i]:位置為sa[i]的字尾和位置為sa[i-1]的字尾的最長公共字首,即排名相鄰的兩個字尾的最長公共字首

        height[rank[i]]:相鄰兩個字尾的最長公共字首

        tax[i]:桶

        tp[i]:基數排序輔助陣列,意義類似於sa[i],即按第二關鍵字順序排在第i位的子串的位置

 

 

rsort()函式:基數排序:用更新sa陣列

void rsort(){//桶排序,每個桶是第一關鍵字,桶內次序是第二關鍵字
    for(int i=0;i<=m;i++)tax[i]=0;//桶清空
    for(int i=1;i<=n;i++)tax[rank[tp[i]]]++;//把子串按照第二關鍵字次序放進桶中,越先放進桶內的第二關鍵字越小
    for(int i=1;i<=m;i++)tax[i]+=tax[i-1];//求桶的字首和
    for(int i=n;i>=1;i--)sa[tax[rank[tp[i]]]--]=tp[i];//
按第二關鍵字倒序從桶中抽出子串,tp[i]即是對應子串的位置 }

suffix()函式:求出sa陣列,rank陣列,並最終求出height陣列

void suffix(){                
    for(int i=1;i<=n;i++) rank[i]=a[i],tp[i]=i;
    int p; m=127; rsort();
                        
    for(int w=1;p<n;w<<=1,m=p){//桶的個數更新
        for(int i=n-w+1,p=0;i<=n;i++) tp[++p]=i;//
長度越界的串第二關鍵字是0(即排在最前面) for(int i=1;i<=n;i++) if(sa[i]>w) tp[++p]=sa[i]-w;//未越界則用上一層的sa求出tp陣列 rsort();swap(rank,tp);rank[sa[1]]=1;p=1;//更新sa陣列,將舊rank轉給tp for(int i=2;i<=n;i++) rank[sa[i]]=cmp(tp,sa[i],sa[i-1],w)?p:++p;//離散化rank(把相等的字串的rank設定為相同),子串相等的用同一個序號 } //求出height陣列 int j,k=0; for(int i=1;i<=n;height[rank[i++]]=k) for(k=k?k-1:k,j=sa[rank[i]-1];a[i+k]==a[j+k];++k) ; }

如何求出height陣列

利用性質height[rank[i]] >= height[rank[i-1]]-1

證明:設suffix[k]是suffix[i-1]前一名的字尾,則其其最長公共字首是height[rank[i-1]],都去掉第一個字元,就變成suffix[k+1]和suffix[i],

   顯然suffix[k+1]排在suffix[i]前面,並且其最長公共字首為height[rank[i-1]]-1(就是去掉了第一個字元,其他是一樣的)

      那麼height[rank[i]] 要麼 就是 suffix[k+1], 要麼比suffix[k+1]排名更靠近suffix[i],所以height[rank[i]] >= height[rank[i-1]]-1

不用直接求height,而是通過height[rank[i]]間接求

//求出height陣列
    int j,k=0;
    for(int i=1;i<=n;height[rank[i++]]=k)
        for(k=k?k-1:k,j=sa[rank[i]-1];a[i+k]==a[j+k];++k)//k=height[rank[i-1]]-1,j是證明中的k,
            ;

cmp()函式:比較兩個二元串是否完全相等

int cmp(int *f,int x,int y,int w){return f[x]==f[y] && f[x+w]==f[y+w];}

 

下面是例題:求字串中出現兩次及以上的子串

/*
字尾陣列:suffix[i]:字串第i-len的子串,即i開始的字尾
         sa[i]:按字典序排在第i個的是第sa[i]個字尾
         rank[i]:第i個字尾按字典序排在rank【i]位
         height[i]=suffix(sa[i-1])和suffix(sa[i])的最長公共字首,即排在第i個字尾和第i-1個字尾的公共字首
性質1:rank[j]<rank[k]==>字尾j...k的最長公共字首是min(height[rank[j]+1],height[rank[j]+2]...height[rank[k]])
    假設字尾j的排名小於k,那麼這些排名連續的字尾的最長字首就是上面公式

性質2:height[rank[i]]>=height[rank[i-1]]-1
證明:設suffix[k]是排在suffix[i-1]前一名的字尾,則按照height陣列定義,其最長公共字首是height[rank[i-1]]
    顯然suffix[k+1]排在suffix[i]前面,並且其最長公共字首為height[rank[i-1]]-1     
    所以height[rank[i]]即suffix[i]和其前一名的最長公共字首,必定不小於height[rank[i-1]]-1

可以按照height[rank[1]],height[rank[2]]...計算順序
所有後綴子串的字首子串即為原串的所有子串

怎麼構造rank和sa陣列?基數排序即可
怎麼構造height陣列?利用性質2即可
*/
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorihtm>
using namespace std;

#define maxn 100005

char ch[maxn],all[maxn];
int sa[maxn],rank[maxn],height[maxn],tax[maxn],tp[maxn],a[maxn],n,m;
//rank[i]:第i個字尾的排名,sa[i]排名i的字尾的位置;
//height[i]排名為i的字尾與排名為i-1的字尾的最長公共字首
//tax[i]基數排序輔助陣列(就是桶),tp[i],rank的輔助陣列(第二關鍵字),類似於sa陣列
//為什麼要增加一個tp陣列,因為前幾次排序後可能有好多個串的排序號是一樣的,這時sa[i]就無法精確表示第i個串是第幾個串
//通過增加tp陣列,tp[i]表示排名i的串是第幾個串,rank[tp[i]]表示i在雙關鍵字排序下的序號(rank[tp[i]]==rank[tp[j]]的情況)
//a 原串
char str[maxn];
void rsort(){//基數排序
    for(int i=0;i<=m;i++) tax[i]=0;//每個桶清空
    for(int i=1;i<=n;i++) tax[rank[tp[i]]]++;//桶上累計
    for(int i=1;i<=m;i++) tax[i]+=tax[i-1];//桶累加求字首和,以此求出所有每個桶對應的序號
    for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i];//找到那個桶的值,對應這個排名的就是第tp[i]個數,之後把桶裡的數減1即可!
}

int cmp(int *f,int x,int y,int w){return f[x]==f[y] && f[x+w]==f[y+w];}//比較兩個關鍵字

void suffix(){
    for(int i=1;i<=n;i++) rank[i]=a[i],tp[i]=i;
    m=127,rsort();//一開始以單個字元為單位,所以m=127
    
    for(int w=1,p=1;p<n;w<<=1,m=p){//倍增法,不斷更新rank
        //w:當前子串長度,m:當前離散後排名種類數
        //tp:第二關鍵字,可以由上一次sa得到

        p=0;
        for(int i=n-w+1;i<=n;i++) tp[++p]=i;//長度越界,第二關鍵字為0,退化為下標即可
        for(int i=1;i<=n;i++) if(sa[i]>w) tp[++p]=sa[i]-w;//排名為p的第二關鍵字是屬於串中下標為a[i]-w的;
        //字串首w位不會變成第二關鍵字

        //要使rank[sa[i]]=i,更新sa,用tp暫存上一輪的rank,離散rank
        rsort(),swap(rank,tp),rank[sa[1]]=p=1;

        //用已經完成的sa來更新rank,並離散化:把相等字串的rank設定為相同
        for(int i=2;i<=n;i++) rank[sa[i]]=cmp(tp,sa[i],sa[i-1],w)?p,++p;
    }
    //求出height陣列
    int j,k=0;
    for(int i=1;i<=n;height[rank[i++]]=k)
        for(k=k?k-1:k,j=sa[rank[i]-1];a[i+k]==a[j+k];++k)//k=height[rank[i-1]]-1,j是證明中的k,
            ;
        
}   
        
        
void init(){        
    scanf("%s",str);    
    n=strlen(str);      
    for(int i=0;i<n;i++)
        a[i+1]=str[i];    
}                       
int main(){            
    init();             
    suffix();           
    int ans=height[2];  

    for(int i=3;i<=n;i++) 
        ans+=max(height[i]-height[i-1],0);
    printf("%d\n",ans);
}
//詢問一個字串中有多少至少出現兩次的子串,
//height[i]是兩個字尾的字首長度,為答案做的貢獻是以字首首字母開始的長度從1到height[i]個子串
//同時該貢獻要減去height[i-1]的貢獻,即如果首字母相同的情況下造成的重複計算
//但是如果這個差小於0,那麼顯然第i個字尾和第i-1個字尾首字母就不相同,那麼這種情況貢獻是0

 

關於height陣列的一些應用:height[i]是排名i和排名i-1的兩個字尾的最長公共字首

1.求一個串中兩個串的最大公共字首:就是height陣列,用rmq預處理,再O(1)查詢

2.一個串中可重疊的重複最長子串:即求任意兩個字尾的最長公共字首,而任意兩個字尾的最長公共字首都是height