[BZOJ3238][AHOI2013]差异 [后缀数组+单调栈]

题目地址 - GO->


题目大意:

给定一个长度为 n 的字符串S,令Ti表示它从第i个字符开始的后缀,求以下这个式子的值:

1i<jnlen(Ti)+len(Tj)2×lcp(Ti,Tj)

其中, len(a) 表示字符串 a 的长度, lcp(a,b) (longest common prefix)表示字符串 a 和字符串 b 的最长公共前缀。


分析:

我们将式子变一下形,可以容易的得到:

[1i<jnlen(Ti)+len(Tj)] 2×[1i<jnlcp(Ti,Tj)]

因为前半部分是求后缀长度和,而后缀长度是递减的,所以可以由等差数列公式得到1inlen(Ti)=n×(n+1)2,而在计算每一个len(Ti)时,它会被算n1次,所以前半部分我们就O(1)得到为

1i<jnlen(Ti)+len(Tj)=(n1)×[1inlen(Ti)]=(n1)×(n×(n1)2)

那么对于后面的,一看数据范围2n500000,总不能O(n2)的计算吧。

所以这里运用了一个巧妙的方法,单调栈

因为我们发现lcp是按照sa(后缀数组)中的rank单调的,而两个后缀之间的lcpheight数组的区间最小值,所以查询两个后缀的lcp可以用rmq预处理,但是对于多个如何处理呢?

所以我们先对所有的下标按照rank排个序,然后往单调栈里面加,单调栈维护的lcp递增不减,每新加入一个lcp(Ti1,Ti)时,我们要计算Ti对于答案的贡献,那么由于栈中的lcp大小是单调递增不减的,所以当加入一个新的lcp时,要把栈顶大于这个新的值的元素给删除,并且还要消除比它大的这些元素的影响,而一个新的lcp的贡献等于之前lcp比它小的贡献加上lcp比它大的个数乘以它的lcp大小,要消除的影响就是每个比它大的lcp×大的个数。所以我们可以用一个单调栈来完成这个操作。

代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const int M=1e6+10;
int n,minv;
int stk[M],top;
char s[M];
int cnt[M],rak[M],y[M],sa[M],A,p,len,h[M],all[M];
ll ans,sumv,now;
void getsa(){
    len=strlen(s)+1;n=len-1;A=256;
    for(int i=0;i<len;i++) ++cnt[rak[i]=s[i]];
    for(int i=1;i<=A;i++) cnt[i]+=cnt[i-1];
    for(int i=len-1;i>=0;i--) sa[--cnt[rak[i]]]=i;
    for(int k=1;k<=len;k<<=1){
        for(int i=0;i<=A;i++) cnt[i]=0;p=0;
        for(int i=len-k;i<len;i++) y[p++]=i;
        for(int i=0;i<len;i++) if(sa[i]>=k) y[p++]=sa[i]-k;
        for(int i=0;i<len;i++) ++cnt[rak[y[i]]];
        for(int i=1;i<=A;i++) cnt[i]+=cnt[i-1];
        for(int i=len-1;i>=0;i--) sa[--cnt[rak[y[i]]]]=y[i];
        swap(rak,y);p=1;rak[sa[0]]=0;
        for(int i=1;i<len;i++){
            if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]){
                rak[sa[i]]=p-1;
            }else{
                rak[sa[i]]=p++;
            }
        }
        if(p>=len) break;A=p;
    }
    for(int i=1;i<=n;i++) rak[sa[i]]=i;
}
void geth(){
    int k=0;
    for(int i=0;i<n;i++){
        if(!rak[i]) continue;
        if(k)--k;
        int j=sa[rak[i]-1];
        for(;s[i+k]==s[j+k];k++);
        h[rak[i]]=k;
    }
    for(int i=n;i>=1;i--) ++sa[i],rak[i]=rak[i-1];
}
int rmq[M][22];
void initrmq(){
    for(int i=1;i<=n;i++) rmq[i][0]=h[i];
    for(int j=1;(1<<j)<=n;j++){
        for(int i=1;i+(1<<j)-1<=n;j++){
            rmq[i][j]=min(rmq[i][j-1],rmq[i+(1<<(j-1))][j-1]);
        }
    }
}
int lcp(int a,int b){
    if(a>n||b>n) return 0;
    if(a==b) return n-a+1;
    ++a;
    if(a>b)swap(a,b);
    int k=0;
    for(;(1<<(k+1))<=b-a+1;k++);
    return min(rmq[a][k],rmq[b-(1<<k)+1][k]);
}
int ls[M];
int main(){
    scanf("%s",s);
    getsa();
    geth();
    initrmq();
    for(int i=1;i<=n;i++) ls[i]=rak[i];
    sort(ls+1,ls+n+1);//按照rank排序
    for(int i=2;i<=n;i++){
        minv=lcp(ls[i-1],ls[i]);//rank相邻的两个lcp是比较大的,因为相似度比较高。
        now=0;
        while(top&&stk[top]>=minv){
            now+=all[top];//累计大的个数
            ans-=(1ll*all[top]*stk[top]);//减去大的影响
            --top;
        }
        stk[++top]=minv;all[top]=now+1;//当前的lcp=minv,个数=前面大的个数+自己。
        ans+=(1ll*stk[top]*all[top]);//加上当前的贡献
        sumv+=ans;//每次统计到答案里面
    }
    printf("%lld
",(1ll*n*(n+1))/2ll*(n-1)-2ll*sumv);
    return 0;
}

这个题,看其他人还有后缀树+虚树,后缀自动机等做法,下面有个相似的题目,也可以用这个方法:BZOJ3879 SVT

若有错,请大佬指出,Orz。

原文地址:https://www.cnblogs.com/VictoryCzt/p/10053434.html