【美团杯2020】半前缀计数(SAM)

前言

史上最通俗的后缀自动机详解:https://www.luogu.com.cn/blog/Kesdiael3/hou-zhui-zi-dong-ji-yang-xie

题意

设小写字母字符串\(s\),长度为\(n\)\(s[l:r]\)表示第\(l\)个到第\(r\)个字符构成的子串。
定义半前缀是\(s[1:i]+s[j:k]\),其中\(0≤i<len(s),i<j≤len(s),j−1≤k≤len(s)\)。直观上来说,你可以把半前缀理解成某一个前缀\(s[1:k]\)删除掉某一个子串后形成的结果(当然也允许不删)。
给出字符串\(s\),你需要求出\(s\)的所有半前缀中,有多少个不同的字符串。

思路

对于一个半前缀\(s[1:i]+s[j:k]\),如果有\(s[1:i+1]+s[j+1:k]\)与其相等即\(s[i+1]=s[j]\)时,则说明这个半前缀重复了。因此,对于每个前缀\(s[1:i]\),我们统计\(s[i+1:n]\)中有多少不同子串不以\(s[i+1]\)开始,把这些加起来就是答案。我们将串倒序加入后缀自动机,这样我们就能够知道每次以\(s[i]\)开始的子串数量。然后对于每个前缀统计答案即可。注意计算空串。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxx = 1e6+10;
struct node
{
    int ch[26];
    int len,fa;
    node(){memset(ch,0,sizeof(ch));len=0;}
}dian[maxx];
int las=1,tot=1;
void add(int c)
{
    int p=las;int np=las=++tot;
    dian[np].len=dian[p].len+1;
    for(;p&&!dian[p].ch[c];p=dian[p].fa)dian[p].ch[c]=np;
    if(!p)dian[np].fa=1;
    else
    {
        int q=dian[p].ch[c];
        if(dian[q].len==dian[p].len+1)dian[np].fa=q;
        else
        {
            int nq=++tot;dian[nq]=dian[q];
            dian[nq].len=dian[p].len+1;
            dian[q].fa=dian[np].fa=nq;
            for(;p&&dian[p].ch[c]==q;p=dian[p].fa)dian[p].ch[c]=nq;
        }
    }
}
char s[maxx];
LL cnt[26];
int main()
{
    scanf("%s",s+1);
    int n=strlen(s+1);
    LL sum=1,ans=1;
    for(int i=n;i>=1;i--)
    {
        add(s[i]-'a');
        LL tmp=dian[las].len-dian[dian[las].fa].len;
        cnt[s[i]-'a']+=tmp;
        sum+=tmp;
        ans+=sum-cnt[s[i]-'a'];
    }
    printf("%lld\n",ans);
    return 0;
}
posted @ 2020-05-25 11:34  灰灰烟影  阅读(309)  评论(0编辑  收藏  举报