SA后缀数组

基础概念:

子串:

在一个字符串s中,取任意i<=j,那么从i到j的这一段就叫做s的一个子串
后缀:

从字符串s的某个位置i到字符串末尾的子串,
suff[i]:

以s的第i个字符为第一个元素的后缀
后缀数组:
现有字符串s,长度为n
把s的全部后缀按照字典序排序,总共有n个后缀
sa[i]:

表示排名为i的后缀的起始位置的下标——根据排名来查询后缀起始位置
rak[i]:

表示起始位置为i的后缀的排名——根据起始位置来查询后缀排名

LCP:
LCP[i][j]:表示suff[sa[i]](排名为i的后缀)与suff[sa[j]](排名为j的后缀)的最长公共前缀的长度
LCP[i][j]=LCP[j][j]
LCP[i][i]=Len[sa[i]]=n-sa[i]+1;
该式子的意思为,自身与自身的最长公共前缀就是自身,其长度为排名为i的后缀的长度,而其长度等于n减去起始位置的下标再加1
LCP性质:
当i<j<k时
LCP[i][j]>=LCP[i][k]
显然由字典序排序的单调性可知,离i越近的后缀与i的前缀重合度就会越大,即长度越长

Height[i]:

表示LCP[i][i-1],即排名为i的后缀与排名为i-1的后缀的最长公共前缀

后缀数组实现:DC3 O(n)
                         DA O(nlogn)

倍增实现:

倍增思路:
对以每个字符为开头的长度为2^k的子字符串进行排序,即为rank值
k从0开始,每次+1
当2^k的长度大于n以后
以第一个字符开头的长度为2^k的子字符串已经包含了整个字符串
所以每个字符开始的长度为2^k的子字符串就相当于所有的后缀
对于靠后的起始字符的子串而言,若其到字符串末尾的长度小于2^k,那么就取它的实际长度
并且我们对这些子字符串都已经用一定的方法比较出了大小
即rank值里面没有相同的值
那么此时的rank值就是最后的结果

那么如何得到这个rank值呢?
我们利用倍增的思想
我们对于n个长度为2^k的子字符串进行rank值排序
当k等于0的时候,子串长度为1,直接按照字典序排出rank值大小即可
假设我们要比较a和b两个长度为2^k的子字符串
只需要将其划分成为两个2^(k-1)长度的子字符串
即,将a划分为a1和a2,将b划分为b1和b2,长度均为2^(k-1)
那么我们先比较a1和b1的rank值大小
若rank[a1]!=rank[a2],则已经比较出a和b的字典序大小
若rank[a1]==rank[a2],我们再进而比较a2和b2的rank值
由于两个子串rank值不可能相同,则一定能够通过rank[a2]和rank[b2]比较出a和b的大小
这样,当2^k>n时,我们就得到了sa数组

后缀数组的应用举例:

后缀数组为排序后的数组,具有一定的单调性

求无重复子串:

对于一个长度为n的字符串而言,其最多有n*(n+1)/2个不重复子串
思考过程:
对于每一个子串,我们可以分其以不同的起始位置讨论
以下标为1的开头的子串有n个
以下标为2的开头的子串有n-1个
以此类推
最后就是一个1+2+3+……+n的求和
故有n*(n+1)/2个子串
对于s中的每一个子串
一定存在于每一个后缀的前缀中
即每一个后缀的前缀都会是一个子串
那么我们只需要将重复的前缀剔除,剩余的后缀所贡献的前缀的总和就是无重复子串的个数
由于字典序排序问题
相邻的两个后缀的前缀重合度一定是最大的

即对于排名为i的后缀而言
其能贡献的子串个数为n-sa[i]+1-Height[i]

求可重叠的k次最长重复子串
由上面第一道题的讨论已知
每一个子串一定存在于每一个后缀的前缀中
那么要求可重复的子串
就是求后缀数组中可重复的前缀
这就是height数组的含义

由于按字典序排序后
后缀的前缀重叠度最大
当i<j<k时
LCP[i][j]>=LCP[i][k]
因此选一个连续的区间的前缀
他们的可重叠度是最大的
单调性——二分——枚举最长长度

题目链接:

https://www.luogu.com.cn/problem/P3809

模版代码:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N=1E6+10;

char s[N];
int wa[N],wb[N],wss[N],wv[N],sa[N];
int cmp(int* r,int a,int b,int l){
    return r[a]==r[b]&&r[a+l]==r[b+l];
}

void da(int* r,int* sa,int n,int M){
    int i,j,p,*x=wa,*y=wb,*t;
    //这里的字符串数组r,传入的是原串s+1
    //从r[0]到r[n-1],长度为n
    //最大值小于M
    //约定除了r[n-1]外的r[i]都大于0,且r[n-1]等于0
    for(i=0;i<n;i++) wss[x[i]=r[i]]++;
    for(i=1;i<M;i++) wss[i]+=wss[i-1];
    //至此,wss[i]表示的是字母关键值为i的字母前面有wss[i]-1个字母,即自己的排名为wss[i]-1
    for(i=n-1;i>=0;i--) sa[--wss[x[i]]]=i;
    //此时wa数组里存的是初始关键值(rank值),即为ASCII码的大小排序
    //按照关键值大小,由后到前地排入sa中,sa的值为i
    //因为x[i]为关键值大小,映射的字母位置为i
    //排名从0到n-1
    //sa[i]的值也是从0到n-1

    //j为倍增子串长度,每次*=2
    //第一次进入该循环时,M仍然是da函数初始化的M
    for(j=1,p=1;p<n;j*=2,M=p){
        //第一次基数排序
        //把长度小于j的后缀的起始字母位置按相对位置不变地记录到y数组中
        //由于他们无法接上第二子串,所以他们第二关键值为0,是最小的,所以排在最前面
        for(p=0,i=n-j;i<n;i++) y[p++]=i;
        //把长度大于等于j的后缀的起始字母位置按相对后缀字典序排名不变地记录到y数组中
        //这里往前错位j个字母,将第sa[i]个字母的关键值给予第sa[i]-j个字母
        //即排名为i的起始字母的关键值是第sa[i]-j,也就是他前j个字母的第二关键值
        for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;

        //清零计数
        for(i=0;i<M;i++) wss[i]=0;
        //wv[i]为关键值大小,映射的字母位置是y[i]
        //wv[i]表示经过第一次基数排序后
        //第二关键值排名为i的后缀的起始字母位置为y[i]
        for(i=0;i<n;i++) wss[wv[i]=x[y[i]]]++;
        for(i=1;i<M;i++) wss[i]+=wss[i-1];
        //在第二关键值排名的基础上进行第一关键值的排名
        for(i=n-1;i>=0;i--) sa[--wss[wv[i]]]=y[i];

        //更新rank数组,即更新x数组
        //由于y数组已经没有用了,马上就会被下次循环覆盖
        //所以我们这里借用y暂时成为旧x来更新x
        //由于以sa[i]开头的长度为j的子串可能相同
        //故rank值可能相同
        //所以需要比较两个字符串是否相同
        //相同的话,把rank值更新为p-1(由于上次p自增过),即与上一个字符串(即与自己相同的字符串)的rank值相同
        //不同的话,更新为p,并p自增
        for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
            x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
        // 这样,就把rank值更新完了,且rank的最大值不会超过p,因此在外层循环中,可以把上界M设为p,加快循环
        //同时,当有n个不同的rank值时,即p==n时
        //说明每个子串均不相同,此时可以结束外层循环
        // cmp函数的解释:
        // 两个排名相邻的子串相同,当且仅当他们第一子串的关键值与第二子串的关键值均相同
        //
    }
}


int cal[N];
int main(){
    scanf("%s",s+1);
    int n=strlen(s+1);
    for(int i=1;i<=n;i++) cal[i]=s[i]-'0'+1;
    //定义cal[n+1]等于0
    //好处在于:
    //当cmp函数判定sa[i-1]和sa[i]的第一子串的关键值时
    //若r[a]==r[b]
    //则说明他们的第一子串一定不包括cal[n+1]这个字符(全场唯一一个0)
    //因此,可以放心地调用r[a+l]与r[b+l],而不用担心越界
    cal[n+1]=0;
    //传入时传入数组长度为n+1,即包括最后的0
    da(cal+1,sa,n+1,'z'-'0'+2);
    //由于cal[n+1]的存在,他一定排在第0位
    //因此sa数组排名从1开始
    //又由于传入的为s+1,因此s+1的下标0对应原字符串s的下标1
    //因此sa[i]+1为对应起始字母位置
    for(int i=1;i<=n;i++) printf("%d ",sa[i]+1);
    return 0;
}
//顺便写上rak数组和height数组的计算方式
int rak[N], height[N];
void calheight(int *r, int *sa, int n)
{
    int i, j, k = 0;
    // k表示最长公共前缀长度
    //求rak
    //由于sa[i]表示的是排名为i的后缀的起始字母的位置
    //所以位置为sa[i]的字母起始的后缀的排名为i
    for (int i = 1; i <= n; i++) rak[sa[i]] = i;

    //求height
    //用到性质:height[i]>=height[i-1]-1
    for(i=0;i<n;height[rak[i++]]=k)
        for(k?k--:0,j=sa[rak[i]-1];r[i+k]==r[j+k];k++)
    //顺便更新sa
    for(i=n;i;i--) rak[i]=rak[i-1],sa[i]++;
}
  • 13
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值