【美团杯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;
}