题目:[TJOI2019]甲苯先生和大中锋的字符串

大中锋有一个长度为 n 的字符串,他只知道其中的一个子串是祖上传下来的宝藏的密码。但是由于字符串很长,大中锋很难将这些子串一一尝试。

这天大中锋找到甲苯先生算命,但是甲苯先生说:“天机不可泄漏”。

在大中锋的苦苦哀求下,甲苯先生告诉大中锋:“密码是在字符串中恰好出现了 kk 次的子串”。

但是大中锋不知道该怎么做,在大中锋再三的恳求下,甲苯先生看其真诚,又告诉他:“在恰好出现了 k 次的子串中,你去按照字串的长度分类,密码就在数量最多的那一类里”。

大中锋为了尝试这个密码,想让你帮忙找出子串长度出现次数最多的长度数(如果有多个输出最长长度)。

题解(后缀数组):

后缀数组好题,题解说是大水题,但我看不出来。

题还是做得太少,对后缀数组的理解也太浅。

我们用一个长度为k的滑块去滑字符串。

字符串的先后顺序是按后缀排序来的。

我们维护了区间的最小height值,记为x,

这是什么?

如果x为0,说明这样的前缀失败了,出现次数不到k

如果x不为0,说明此时长度为x的前缀至少有k个

但我们要求恰好为k的个数

那怎么办呢?

有了,我们判一下,h[i-k+1]和h[i+1]

如果这两个值中有值大于等于x,这说明此时长度为x的前缀超过了k个

否则这样长度为x的前缀恰好等于k个。

考虑前缀中能分出多少的前缀恰好等于k,最小的就是max(h[i-k+1],h[i+1])+1,最大的是x

计算答案就差分一下即可,维护最大值 .

代码:

#include
using namespace std;
#define next Next
const int N=4e5+5;
int n,m,k,q[N],cf[N],sa[N],height[N],x[N],y[N],c[N],rk[N];
char a[N];
/*char buf[1<<21],*p1=buf,*p2=buf;
inline int gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}*/
#define gc getchar
inline int read()
{
    int ret=0,f=0;char c=gc();
    while(!isdigit(c)){if(c=='-')f=1;c=gc();}
    while(isdigit(c)){ret=ret*10+c-48;c=gc();}
    if(f)return -ret;return ret;
}
void SA(int n,int m)
{
    int p;
    for(int i=0;i<=n;i++)
    {
        sa[i]=rk[i]=x[i]=y[i]=0;
    }
    for(int i=1;i<=m;i++)c[i]=0;
    for(int i=1;i<=n;i++)c[x[i]=a[i]]++;
    for(int i=1;i<=m;i++)c[i]+=c[i-1];
    for(int i=n;i;i--)sa[c[x[i]]--]=i;
    for(int i=1;i;i=i*2)
    {
        p=0;
        for(int j=n-i+1;j<=n;j++)y[++p]=j;
        for(int j=1;j<=n;j++)
            if(sa[j]>i)y[++p]=sa[j]-i;
        for(int j=1;j<=m;j++)c[j]=0;
        for(int j=1;j<=n;j++)c[x[y[j]]]++;
        for(int j=1;j<=m;j++)c[j]+=c[j-1];
        for(int j=n;j;j--)sa[c[x[y[j]]]--]=y[j];
        swap(x,y);
        x[sa[1]]=1;
        p=2;
        for(int j=2;j<=n;j++)
        {
            if(y[sa[j]]==y[sa[j-1]]&&y[sa[j]+i]==y[sa[j-1]+i])x[sa[j]]=p-1;
            else x[sa[j]]=p++;
        }
        if(p>n)return;
        m=p;
    }
}
void Height()
{
    int k=0;
    for(int i=1;i<=n;i++)rk[sa[i]]=i;
    for(int i=1;i<=n;i++)
    {
        if(rk[i]==1)continue;
        if(k)k--;
        int j=sa[rk[i]-1];
        while(i+k<=n&&j+k<=n&&a[i+k]==a[j+k])k++;
        height[rk[i]]=k;
    }
}
signed main()
{
    int T=read();
    while(T--)
    {
        memset(cf,0,sizeof(cf));
        scanf("%s",a+1);
        n=strlen(a+1);
        k=read();
        height[n+1]=0;
        SA(n,122);
        Height();
        int l=1,r=0;
        for(int i=1;i<=k;i++)
        {
            while(l=height[i])r--;
            q[++r]=i;
        }
        for(int i=k;i<=n;i++)
        {
            if(i-q[l]+1>=k)l++;
            while(l=height[i])r--;
            q[++r]=i;
            int R;
            if(k==1)R=n-sa[i+k-1]+1;
            else R=height[q[l]];
            int L=max(height[i-k+1],height[i+1]);
            if(L<R)
            {
                cf[L+1]++;
                cf[R+1]--;
            }
        }
        int sum=cf[0],ma=1,ans=-1;
        for(int i=1;i<=n;i++)
        {
            sum+=cf[i];
            if(sum>=ma)
            {
                ma=sum;
                ans=i;
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}
/*
后缀数组好题,题解说是大水题,但我看不出来。
题还是做得太少,对后缀数组的理解也太浅。
我们用一个长度为k的滑块去滑字符串。
字符串的先后顺序是按后缀排序来的。
我们维护了区间的最小height值,记为x,
这是什么?
如果x为0,说明这样的前缀失败了,出现次数不到k
如果x不为0,说明此时长度为x的前缀至少有k个
但我们要求恰好为k的个数
那怎么办呢?
有了,我们判一下,h[i-k+1]和h[i+1]
如果这两个值中有值大于等于x,这说明此时长度为x的前缀超过了k个
否则这样长度为x的前缀恰好等于k个。
考虑前缀中能分出多少的前缀恰好等于k,最小的就是max(h[i-k+1],h[i+1])+1,最大的是x
计算答案就差分一下即可,维护最大值 .
*/