基数排序和后缀数组的原理入门

字符串里算难了。。参考了好多博客

推荐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
",ans);
}
//询问一个字符串中有多少至少出现两次的子串,
//height[i]是两个后缀的前缀长度,为答案做的贡献是以前缀首字母开始的长度从1到height[i]个子串
//同时该贡献要减去height[i-1]的贡献,即如果首字母相同的情况下造成的重复计算
//但是如果这个差小于0,那么显然第i个后缀和第i-1个后缀首字母就不相同,那么这种情况贡献是0

关于height数组的一些应用:height[i]是排名i和排名i-1的两个后缀的最长公共前缀

因为height数组是两个排名相邻的串的前缀,那么全串的最长公共前缀必是max{height[i]}

1.求一个串中两个串的最大公共前缀:

  就是height数组,用rmq预处理,再O(1)查询,

2.一个串中可重叠的重复最长子串:即求任意两个后缀的最长公共前缀,而任意两个后缀的最长公共前缀都是height