后缀自动机
想到了其实挺简单的
首先对于后缀的前缀,我们不太好维护,所以我们可以考虑将字符串倒过来,这样就变成了维护前缀的后缀!
那么我们自然就想到了后缀自动机
然后我们观察这个式子发现恰好是是求parent树上任意两点路径和,那么我们在parent树上算一下每条边的贡献就好了。
对于一条边,他的贡献就是(len[x]-len[fa[x]])*si[x]*(n-si[x])
现在我们来细说一下为什么这个式子发现恰好是是求parent树上任意两点路径和
对于每条边来说,结束记父亲为fa,记儿子为son,那么fa一定是son的一个后缀,那么是后缀的这一段肯定不贡献,所有贡献的长度就是len[son]-len[fa],然后我们看贡献多少,首先有endpos(son)这么多位置fa一定是以这些位置为结尾的字符串的后缀,而有n-endpos(son)这么多位置可以作为开头,所以这条边的贡献就是上面说的那个式子!
时间效率:O(n)
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;
const int maxn=1e6+10;
int n;
char s[maxn];
struct suda
{
struct da{int ch[30],fa,len;}po[maxn];
int si[maxn],st[maxn],nt[maxn],to[maxn],topt,las,tot;
long long ans;
void init()
{
memset(po,0,sizeof po);
memset(st,0,sizeof st);
memset(si,0,sizeof si);
las=tot=1; ans=0;
}
void insert(int x)
{
int p=las,np=las=++tot; po[np].len=po[p].len+1; si[np]=1;
for (;p&&(!po[p].ch[x]);p=po[p].fa) po[p].ch[x]=np;
if (!p) po[np].fa=1;
else
{
int q=po[p].ch[x];
if (po[q].len==po[p].len+1) po[np].fa=q;
else
{
int nq=++tot; po[nq]=po[q];
po[nq].len=po[p].len+1;
po[q].fa=po[np].fa=nq;
for (;p&&(po[p].ch[x]==q);p=po[p].fa) po[p].ch[x]=nq;
}
}
}
void add(int x,int y){to[++topt]=y; nt[topt]=st[x]; st[x]=topt;}
void getsi(int x)
{
int p=st[x];
while (p)
{
getsi(to[p]);
si[x]+=si[to[p]];
p=nt[p];
}
if (si[x]!=0) ans+=1ll*(po[x].len-po[po[x].fa].len)*si[x]*(n-si[x]);
}
long long solve()
{
for (int i=2;i<=tot;i++) add(po[i].fa,i);
getsi(1); return ans;
}
}SAM;
int main()
{
scanf("%s",s+1); n=strlen(s+1); SAM.init();
for (int i=1;i<=n;i++) SAM.insert(s[n-i+1]-'a');
printf("%lld\n",SAM.solve());
return 0;
}