【美团杯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
",ans);
    return 0;
}
原文地址:https://www.cnblogs.com/HooYing/p/12955955.html