HDU 4622 Reincarnation - 三种解法对比

1 minute read

Published:

题意: 长度为2000的串,10000个询问:[l,r]这个子串包含多少不同的子串。

比赛时用的后缀数组 o(nQ),赛后用的 hash o(nn*log(n)) 和后缀自动机,这个log的出现是因为hash写不来 = =。

解法一:后缀数组

先对原来的串做一遍后缀数组,对于每个询问,对sa数组进行扫描,如果当前前缀在[l,r]范围内,就找到之前和他公共前缀最长且也在[l,r]范围内的串,lcp最长公共前缀,答案加上串在当前询问中的长度减去最长公共前缀。

值得注意的是和当前串公共前缀最长的并不就是上一次在[l,r]范围内的串,也有可能是再之前,原因是询问的区间,相当于给每个串截断了一部分,所以区间里sa数组不再代表这区间里的串的排名,处理比较容易,见代码。

#include <stdio.h>
#include <string.h>
#include <vector>
#include <iostream>
#include <math.h>
#include <algorithm>
#include <set>
#define ll long long
#define clr(a,b) memset(a,b,sizeof(a));
using namespace std;

const int N = 2010;
int ua[N],ub[N],us[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=ua,*y=ub,*t;
    for(i=0;i<m;i++)us[i]=0;
    for(i=0;i<n;i++)us[x[i]=r[i]]++;
    for(i=1;i<m;i++)us[i]+=us[i-1];
    for(i=n-1;i>=0;i--)sa[--us[x[i]]]=i;
    for(j=1,p=1;p<n;j*=2,m=p){
        for(p=0,i=n-j;i<n;i++)y[p++]=i;
        for(i=0;i<n;i++)if(sa[i]>=j)y[p++]=sa[i]-j;
        for(i=0;i<m;i++)us[i]=0;
        for(i=0;i<n;i++)us[x[i]]++;
        for(i=1;i<m;i++)us[i]+=us[i-1];
        for(i=n-1;i>=0;i--)sa[--us[x[y[i]]]]=y[i];
        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++;
    }
}
int rank[N],height[N];
void calheight(int *r,int *sa,int n){
    int i,j,k=0;
    for(i=1;i<=n;i++)rank[sa[i]]=i;
    for(i=0;i<n;height[rank[i++]]=k)
        for(k?k--:0,j=sa[rank[i]-1];r[i+k]==r[j+k];k++);
}
int *RMQ=height;
int mm[N];
int best[20][N];
void initRMQ(int n){
    int i,j,a,b;
    for(mm[0]=-1,i=1;i<=n;i++)
        mm[i]=((i&(i-1))==0)?mm[i-1]+1:mm[i-1];
    for(i=1;i<=n;i++)best[0][i]=i;
    for(i=1;i<=mm[n];i++)for(j=1;j<=n+1-(1<<i);j++){
        a=best[i-1][j];
        b=best[i-1][j+(1<<(i-1))];
        if(RMQ[a]<RMQ[b])best[i][j]=a;
        else best[i][j]=b;
    }
}
int askRMQ(int a,int b){
    int t;
    t=mm[b-a+1];b-=(1<<t)-1;
    a=best[t][a];b=best[t][b];
    return RMQ[a]<RMQ[b]?a:b;
}
int lcp(int a,int b){
    int t;
    a=rank[a],b=rank[b];
    if(a>b){t=a;a=b;b=t;}
    return height[askRMQ(a+1,b)];
}
int t,r[N],m,len[N],sa[N];
char a[N];
int main(){
//    freopen("/home/axorb/in","r",stdin);
    scanf("%d",&t);
    while(t--){
        scanf("%s",a);
        int l=strlen(a);
        for(int i=0;i<l;i++){
            r[i]=a[i];
            len[i]=l-i;
        }
        r[l]=0;len[l]=0;
        da(r,sa,l+1,256);
        calheight(r,sa,l);
        initRMQ(l);
        scanf("%d",&m);
        while(m--){
            int q,w;
            scanf("%d%d",&q,&w);
            q--,w--;
            int o=w-q+1;
            int last=-1;
            int ans=0;
            for(int i=1,p=0;i<=l&&p<o;i++)if(sa[i]>=q&&sa[i]<=w){
                p++;
                if(last==-1)ans+=min(len[sa[i]],w-sa[i]+1);
                else{
                    int h=min(lcp(sa[i],sa[last]),w-sa[last]+1);
                    ans+=min(len[sa[i]],w-sa[i]+1)-min(h,w-sa[i]+1);
                }
                if(last!=-1&&w-sa[last]+1 > w-sa[i]+1){
                    int he = lcp(sa[last],sa[i]);
                    if(he >= w-sa[i]+1) last = last;
                    else last = i;
                }                                    //维护之前公共前缀最长的串,只要判断当前串是否是last的前缀,是last不变,不是last就为i
                else last = i;
            }
            printf("%d\n",ans);
        }
    }
    return 0;
}

解法二:Hash

用hash把所有相同的子串放在一起,[l1,r1] [l2,r2]…。现在对于每一个询问,只把这个串第一次出现的算是这个询问的,也就是询问[L,R],满足 l1<L<=l2, R>=r2 时 [l2,r2] 算给他。

然后利用扫描线和部分和维护,对于子串[l2,r2],g[r2][l1+1] 加1,g[r2][l2+1] 减1,对于所有r,从左到右求一遍部分和,再从上到下求一遍部分和,g[r][l]就是询问l,r的答案了。

解法三:后缀自动机

预处理,解法不多说(详见后缀自动机教程)。


Originally written in 2013, comparing three different approaches to solve the same problem.