后缀数组

sa

终于学习了一波sa
sa的大概思想其实就是先把每一个i开头,长度为1的字符串排序,再通过前面的结果,用倍增的方法求出每一个i开头,长度为 2k 的字符串的排名
设sa[i]表示排名第i的后缀开头的位置,rank[i]为以i开头的后缀的排名,height[i]表示sa[i]和sa[i-1]的lcp(最长公共前缀)
假设现在我们在对以i开头,长度为k的字符串排序,那么我们是已经知道长度为k/2长度的字符串的排名了的,设x[i]表示[i..i+k-1]这个字符串在所有长度为k的字符串的排名,y[i]表示排名为i的字符串[j+k..j+k+k-1]的j
那么x数组是已知的,因为前面已经求过了,那么我们现在考虑一下求y数组
设s[0..len-1]
首先j的位置在[len-k..len-1]这一段的对应的字符串是空的,那么它们肯定是最小的
其他位置对应的名次我们可以通过sa数组来求
如果有一个sa[i]的值是>=k的,那么我们就有一个j对应sa[i]所表示的那个字符串了,比如sa[i]=3,k=2,那么j=1就等价于sa[i]所表示的字符串
观察一下sa数组的定义,发现sa已经把全部的k/2长度的字符串排序了,那么我们只要从小到大枚举sa[i]中的i,所得到的一系列的j对应的字符串其实也是有序的,那么只要将他们依次放入y数组里面就好了
现在我们考虑一下使用基数排序通过当前的排序把长度为k的字符串进行排序
考虑一下这个问题的本质,如果要比较i,j的排名,就是先比较x的大小,如果x相同再比较在y中的排名
先把每一个x[i]放入一个名为buc的桶里面(其实为什么网上面的程序打的都是什么x[y[i]]搞到懵逼了那么久虽然是我太菜了),那么我们就相当于已经把x[i]不同的分开了,假设某一个x[i]一共有p个,那么它们的排名就是[t+1..t+p],现在要将他们排序,就可以从后到前枚举每一个x[y[i]]的i,那么我们先枚举到的就是排名最大的t+p名,然后每一个依次减小,这样就可以快速的进行排序了
最后为了节省空间,我们还要重新给每一个新的x[i](即长度为k对应的x)进行一个类似压缩的操作,如果sa[i]对应的字符串等于sa[i-1]对应的字符串,那么就有x[sa[i]]=x[sa[i-1]],否则x[sa[i]]=x[sa[i-1]]+1,最后如果x[sa[len]](sa从1开始排名)已经等于len了那么所有的后缀都已经排好序了,可以直接退出

sa代码

void gesa(){
    len=strlen(s);
    memset(y,0,sizeof(y));
    m=199;
    fo(i,1,len) buc[i]=0;
    fo(i,0,len-1) x[i]=s[i];
    fo(i,0,len-1) buc[x[i]]++;
    fo(i,1,len) buc[i]+=buc[i-1];
    fo(i,0,len-1) sa[buc[x[i]]--]=i;
    for(k=1;k<=len;k=k*2){
        p=0;
        fo(i,len-k,len-1) y[p++]=i;
        fo(i,1,len) if (sa[i]>=k) y[p++]=sa[i]-k;
        fo(i,1,m) buc[i]=0;
        fo(i,0,len-1) buc[x[i]]++;
        fo(i,1,m) buc[i]+=buc[i-1];
        for(i=len-1;i>=0;i--) sa[buc[x[y[i]]]--]=y[i];
        fo(i,0,len-1) y[i]=x[i];
        p=1; x[sa[1]]=1;
        fo(i,2,len){
            if (y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=p;
            else x[sa[i]]=++p;
        }
        m=p;
        if (m>=len) break;
    }
    fo(i,1,len) rank[sa[i]]=i;
}

height

height的定义是sa[i-1]和sa[i]开头的后缀的lcp,求这个东西似乎要 n2 ,但是因为一个性质,可以把这个东西优化成O(n)级别的
设k=sa[rank[i]-1],即排名为i开头的后缀的排名-1的后缀,现在我们已经知道他们的lcp为height[rank[i]],现在我们要求height[rank[i+1]],那么有height[rank[i+1]]>=height[rank[i]]-1
为什么呢?觉得网上的证明十分的难懂,然后就自己举了一个例子模拟了一下
现在要求字符串ababa的height,假设现在已经知道了height[rank[1]] (这个东西等于3)现在我们要求height[rank[2]],那么我们发现height[rank[1]]表示的是有一个后缀的前三位与从第一位开始的后缀的后缀的前三位相同,那么就是说一定有一个后缀与从第二位开始的后缀有2位相同
抽象一点的说法:现在已经知道了i开头的字符串和k开头的字符串的lcp=t,那么起码有一个以k+1开头的字符串和以i+1开头的字符串的lcp=t-1
在有这个条件的情况下,我们就可以用很少的时间依次求出height[rank[i]]了

贴代码

#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<cstdio>
#define fo(i,a,b) for(i=a;i<=b;i++)
using namespace std;

const int maxn=100005;

int sa[maxn],rank[maxn],height[maxn],x[maxn],y[maxn],buc[maxn];
int i,j,k,l,n,m,len,p,t1,t2;
char s[maxn];

void gesa(){
    len=strlen(s);
    memset(y,0,sizeof(y));//注意这句话,在有多组数据时要加
    m=199;
    fo(i,1,len) buc[i]=0;
    fo(i,0,len-1) x[i]=s[i];
    fo(i,0,len-1) buc[x[i]]++;
    fo(i,1,len) buc[i]+=buc[i-1];
    fo(i,0,len-1) sa[buc[x[i]]--]=i;
    for(k=1;k<len;k=k*2){
        p=0;
        fo(i,len-k,len-1) y[p++]=i;
        fo(i,1,len) if (sa[i]>=k) y[p++]=sa[i]-k;
        fo(i,1,m) buc[i]=0;
        fo(i,0,len-1) buc[x[i]]++;
        fo(i,1,m) buc[i]+=buc[i-1];
        for(i=len-1;i>=0;i--) sa[buc[x[y[i]]]--]=y[i];
        fo(i,0,len-1) y[i]=x[i];
        p=1; x[sa[1]]=1;
        fo(i,2,len){
            if (y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=p;
            else x[sa[i]]=++p;
        }
        m=p;
        if (m>=len) break;
    }
    fo(i,1,len) rank[sa[i]]=i;
}
void gehei(){
    k=0;
    fo(i,0,len-1){
        if (rank[i]==1){
            height[rank[i]]=0; continue;
        }
        if (k) k--;
        t1=i+k; t2=sa[rank[i]-1]+k;
        while (t1<len && t2<len){
            if (s[t1]==s[t2]) k++; else break;
            t1++; t2++;
        }
        height[rank[i]]=k;
    }
}
int main(){
    //freopen("s.in","r",stdin);
    //freopen("s.out","w",stdout);
    scanf("%s",s);
    gesa();
    fo(i,1,len) printf("%d ",sa[i]+1);
    gehei();
    printf("\n");
    fo(i,2,len) printf("%d ",height[i]);
}

一些题目

两个字符串的最长公共子串

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值