后缀数组学习笔记

后缀数组学习笔记


说在前边

  1. 学习了《后缀数组——处理字符串的有力工具》终于感觉入门了,就总结一下,主要是应用
  2. 原理讲解学习了 大佬Blog

一些性质

height数组:定义height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。那么对于j和k,不妨设rank[j]<rank[k],则有以下性质:
suffix(j)和suffix(k)的最长公共前缀为height[rank[j]+1],height[rank[j]+2],height[rank[j]+3],……,height[rank[k]]中的最小值。

例1: 最长公共前缀

做法:height[rank[j]+1],height[rank[j]+2],height[rank[j]+3],……,height[rank[k]]中的最小值,RMQ解决

#include <bits/stdc++.h>
const int N = 100005;
using namespace std;
char ch[N],str[N];
int SA[N],rnk[N],Height[N],tax[N],tp[N],a[N],n,m;
void RSort() {
    for(int i=0;i<=m;++i) tax[i]=0;
    for(int i=1;i<=n;++i) ++tax[rnk[tp[i]]];
    for(int i=1;i<=m;++i) tax[i]+=tax[i-1];
    for(int i=n;i>=1;--i) SA[tax[rnk[tp[i]]]--]=tp[i];
}
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) rnk[i]=a[i],tp[i]=i;
    m = 127, RSort();
    for(int w=1,p=1,i;p<n;w+=w,m=p) {
        for(p=0,i=n-w+1;i<=n;++i) tp[++p]=i;
        for(i=1;i<=n;++i) if(SA[i]>w) tp[++p] = SA[i]-w;
        RSort(), swap(rnk,tp),rnk[SA[1]]=p=1;
        for(i=2;i<=n;++i) rnk[SA[i]]=cmp(tp,SA[i],SA[i-1],w)?p:++p;
    }
    int j,k=0;
    for(int i=1;i<=n;Height[rnk[i++]]=k)
        for(k=k?k-1:k,j=SA[rnk[i]-1];a[i+k]==a[j+k];++k);
}
void init() {
    scanf(" %s",str);
    n = strlen(str);
    for(int i=0;i<n;++i) a[i+1] = str[i];
}
int Log[N],rmq[N][30];
void init_rmq() {
    Log[1] = 0;
    for(int i=2;i<=n;++i) Log[i] = Log[i>>1] + 1;
    for(int i=1;i<=n;++i) rmq[i][0] = Height[i];
    for(int j=1;j<=20;++j)
        for(int i=1;i+(1<<(j-1))<=n;++i)
            rmq[i][j] = min(rmq[i][j-1],rmq[i+(1<<j-1)][j-1]);
}
int RMQ_mn(int l,int r){
    int L=Log[r-l+1];
    return min(rmq[l][L],rmq[r-(1<<L)+1][L]);
}
int ask(int x,int y) {
    x=rnk[x],y=rnk[y];
    if(x>y)swap(x,y);
    return RMQ_mn(x+1,y);
}
int main() {
    init();
    Suffix();
    init_rmq();
    int q;
    scanf("%d",&q);
    while(q--) {int x,y;
        scanf("%d%d",&x,&y);
        printf("%d\n",ask(x,y));
    }
    return 0;
}

例2:可重叠最长重复子串

题意:给定一个字符串,求最长重复子串,这两个子串可以重叠。
做法:求height数组里的最大值。首先求最长重复子串,等价于求两个后缀的最长公共前缀的最大值。因为任意两个后缀的最长公共前缀都是height数组里某一段的最小值,那么这个值一定不大于height数组里的最大值。所以最长重复子串的长度就是height数组里的最大值。这个做法的时间复杂度为O(n)。

例3:不可重叠最长重复子串

题意:给定一个字符串,求最长重复子串,这两个子串不能重叠。
做法:先二分答案,把题目变成判定性问题:判断是否存在两个长度为k的子串是相同的,且不重叠。解决这个问题的关键还是利用height数组。把排序后的后缀分成若干组,其中每组的后缀之间的height值都不小于k。然后对于每组后缀,只须判断每个后缀的sa值的最大值和最小值之差是否不小于k。如果有一组满足,则说明存在,否则不存在。整个做法的时间复杂度为O(nlogn)。

例4:可重叠的 k次最长重复子串

题意:给定一个字符串,求至少出现k次的最长重复子串,这k个子串可以重叠。
做法:二分答案,然后将后缀分成若干组。不同的是,这里要判断的是有没有一个组的后缀个数不小于k。如果有,那么存在k个相同的子串满足条件,否则不存在。这个做法的时间复杂度为 O(nlogn)。

例5:不相同的子串的个数

题意:给定一个字符串,求不相同的子串的个数。
做法:每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。如果所有的后缀按照suffix(sa1),suffix(sa[2]),suffix(sa[3]),……,suffix(sa[n])的顺序计算,不难发现,对于每一次新加进来的后缀suffix(sa[k]),它将产生n-sa[k]+1个新的前缀。但是其中有height[k]个是和前面的字符串的前缀是相同的。所以suffix(sa[k])将“贡献”出n-sa[k]+1-height[k]个不同的子串。累加后便是原问题的答案。这个做法的时间复杂度为O(n)。

#include <bits/stdc++.h>
const int N = 100005;
using namespace std;
char ch[N],str[N];
int SA[N],rnk[N],Height[N],tax[N],tp[N],a[N],n,m;
void RSort() {
    for(int i=0;i<=m;++i) tax[i]=0;
    for(int i=1;i<=n;++i) ++tax[rnk[tp[i]]];
    for(int i=1;i<=m;++i) tax[i]+=tax[i-1];
    for(int i=n;i>=1;--i) SA[tax[rnk[tp[i]]]--]=tp[i];
}
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) rnk[i]=a[i],tp[i]=i;
    m = 127, RSort();
    for(int w=1,p=1,i;p<n;w+=w,m=p) {
        for(p=0,i=n-w+1;i<=n;++i) tp[++p]=i;
        for(i=1;i<=n;++i) if(SA[i]>w) tp[++p] = SA[i]-w;
        RSort(), swap(rnk,tp),rnk[SA[1]]=p=1;
        for(i=2;i<=n;++i) rnk[SA[i]]=cmp(tp,SA[i],SA[i-1],w)?p:++p;
    }
    int j,k=0;
    for(int i=1;i<=n;Height[rnk[i++]]=k)
        for(k=k?k-1:k,j=SA[rnk[i]-1];a[i+k]==a[j+k];++k);
}
void init() {
    scanf(" %s",str);
    n = strlen(str);
    for(int i=0;i<n;++i) a[i+1] = str[i];
}
int main() {
    int T;
    scanf("%d",&T);
    while(T--) {
        init();
        Suffix();
        int ans = 0;
        for(int i=1;i<=n;++i) ans+=max(n-SA[i]+1-Height[i],0);
        printf("%d\n",ans);
    }
    return 0;
}

例6:最长回文子串

题意:给定一个字符串,求最长回文子串。
做法:穷举每一位,然后计算以这个字符为中心的最长回文子串。注意这里要分两种情况,一是回文子串的长度为奇数,二是长度为偶数。两种情况都可以转化为求一个后缀和一个反过来写的后缀的最长公共前缀。具体的做法是:将整个字符串反过来写在原字符串后面,中间用一个特殊的字符隔开。这样就把问题变为了求这个新的字符串的某两个后缀的最长公共前缀。

例7:连续重复子串

题意:给定一个字符串L,已知这个字符串是由某个字符串S重复R次而得到的,求R的最大值。
做法:穷举字符串S的长 k,然后判断是否满足。判断的时候,先看字符串L的长度能否被k整除,再看suffix(1)和suffix(k+1)的最长公共前缀是否等于n-k。在询问最长公共前缀的时候,suffix(1)是固定的,所以RMQ问题没有必要做所有的预处理,只需求出height数组中的每一个数到height[rank1]之间的最小值即可。整个做法的时间复杂度为O(n)。

posted @ 2018-07-17 14:48  RRRR_wys  阅读(233)  评论(0编辑  收藏  举报