后缀数组(使用快速排序)

lcp[i]代表的是后缀Suffix[sa[i]]后缀Suffix[sa[i+1]]的最长公共前缀, 而网上比较流行的那个模板的height[i]则是后缀Suffix[i]与Suffix[sa[i-1]]的最长公共前缀, 挑战上的模板速度慢一些, 但是好写很多, 并且因为用的是快速排序数值范围比较大的时候不需要离散化

原理:

为了方便处理一些情况, 我们把空串也当做一个后缀, 所以长度为n的字符串有n+1个后缀. 
sa[i] 表示以S[sa[i]]开头的后缀是字符串所有后缀中字典序第i的. 
rank[i]表示以S[i]开头的字符串在字符串所有后缀中排第rank[i]位. 
用倍增法计算后缀数组. 
记rk[i, j]为子串S[i…i+2j2j-1]在所有长度为2j2j长度的子串中的字典序排名, 如果i+2j2j-1超过了字符串的末尾, 则为S[i, n-1]. 
如果我们知道了所有的rk[i, j], 那么, 所有的rk[i, j+1]可以通过rk[i, j]和rk[i+2j2j, j]快速计算出来. 
而一开始的rk[i, 0]是长度为1的子串, 可以直接通过每个字符的编码值得到(rk[i, 0] = int(S[i]);).

效率O(nlog2n)

代码:

bool compare_sa(int i,int j)
{
    if(rankk[i]!=rankk[j]){
        return rankk[i]<rankk[j];
    }else{
        int ri=i+k<=n?rankk[i+k]:-1;
        int rj=j+k<=n?rankk[j+k]:-1;
        return ri<rj;
    }
}

void construct_sa()
{
    n=s.size();
    for(int i=0;i<=n;i++){
        sa[i]=i;
        rankk[i]=i<n?s[i]:-1;
    }
    for(k=1;k<=n;k<<=1){
        sort(sa,sa+n+1,compare_sa);
        tmp[sa[0]]=0;
        for(int i=1;i<=n;i++){
            tmp[sa[i]]=tmp[sa[i-1]]+(compare_sa(sa[i-1],sa[i])?1:0);
        }
        for(int i=0;i<=n;i++){
            rankk[i]=tmp[i];
        }
    }
}

应用

基于后缀数组的字符串匹配

通过预处理目标串的后缀数组, 得到所有后缀的字典序排名, 然后就可以用二分搜索目标串在所有后缀中的位置得到匹配情况.

效率: 预处理O(|S|log2|S|)O(|S|log2|S|), 匹配O(|T|log|S|)

代码:

bool contain()
{
    int left=0,right=s.length();
    while(left!=right){
        int mid=(left+right)>>1;
        if(s.compare(sa[mid],ss.length(),ss)>0){
            right=mid;
        }else if(s.compare(sa[mid],ss.length(),ss)<0){
            left=mid+1;
        }else{
            left=mid;
            break;
        }
    }
    return s.compare(sa[left],ss.length(),ss)==0;
}

高度数组

原理

高度数组是有后缀数组中相邻两个后缀的最长公共前缀的长度组成的数组. 
lcp[i]表示后缀S[sa[i]…]和后缀S[sa[i+1]…]的最长公共前缀的长度位lcp[i] 
利用类似于尺取法的思想可以快速求出高度数组. 
从0开始, 计算后缀S[i…]与后缀S[sa[rank[i]-1]…]的lcp[i], sa[rank[i]-1]是后缀数组中在后缀S[i…]前面一位的后缀, 因为第一个后缀数组中第一个后缀是空串, 所有非空后缀一定有前一个后缀, 所以不用担心sa[rank[i]-1]不存在. 
然后求S[i+1…]和S[sa[rank[i+1]-1…]的lcp[i+1]时, 而S[i+1..]相当于S[i…]去掉第一个字母, 而S[sa[rank[i+1]-1…]虽然不一定是S[sa[rank[i]-1]+1…], 但是, 他们前面lcp[i]-1一定是相同的, 所以计算前缀只需要从i+1+lcp[i]-1开始比较.

复杂度: 计算SA数组O(nlog2n)+计算LCP数组O(n)

代码:


int lcp[maxn];

void construct_lcp()
{
    for(int i=0;i<=n;i++){
        rankk[sa[i]]=i;
    }
    int h=0;
    lcp[0]=0;
    for(int i=0;i<n;i++){
        int j=sa[rankk[i]-1];
        if(h>0){
            h--;
        }
        for(;j+h<n&&i+h<n;h++){
            if(s[j+h]!=s[i+h]){
                break;
            }
        }
        lcp[rankk[i]-1]=h;
    }
}

应用

任意两个后缀的最长公共前缀

LCP数组中存的是在SA数组相邻后缀的公共前缀, 但有一个规律: 后缀S[i...], S[j...](rank[i]<rank[j])的公共前缀长度为min{lcp[rank[i]], lcp[rank[i]+1], ... , lcp[rank[j]-1]}
所所以只要利用RMQ算法就可以快速求出任意两个后缀的公共前缀.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值