有一个比较显然的思路:
先把每个串插入到字典树中建立AC自动机
枚举每个字符串做为t串,求f(s,t),我们知道AC自动机上fail[i]即:根节点到i节点路径上字符串最长的(当前字符串的后缀)与模式串的前缀匹配。类似于于KMP里的next。所以枚举了一个字符串t,s串一定在t串结尾字符节点u的fail树祖先链上。
我们沿fail树链求贡献即可。
但有个问题:
上图是样例的字典树,红线是fail数组。
绿线枚举t串为aba时,遍历6号节点的fail树链。
贡献为: 在6号节点,有3*3=9
在5号节点有:2*2=4;
在2号节点有1*1*2=2; 我们发现这里就出问题了! 因为2号节点的儿子中包含了aba这个字符串,而这个字符串做为s的前缀与t最长后缀匹配已经在6号节点处理过了。这里会多算。
为了解决这个问题,我们可以记录每个节点属于哪几个字符串。然后枚举t串,找s串时:每计算一个串做为s,就把他vs掉,当前的前缀是于t后缀的最长匹配,后面再遇到这个串不进行计算即可。
最后是复杂度证明:看起来这是n^2的,其实是On*sqrt(n)的:
最坏是一条全部字符一样的链:
a
aa
aaa
……
运行次数:
大约是 1e8
实际只跑了200ms,KMP要2000ms
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define pb push_back
const int M = 1e6+7;
const int mod =998244353 ;
int ed[M],pr[M];
ll sm=0;
struct AC
{
int tr[M][26],flag[M],fail[M],nm[M];
vector<int>g[M];int vs[M];
int cnt=1;queue<int>q;
void in(char *s,int id)
{
int len=strlen(s),u=1;
for(int i=0;i<len;i++)
{
int v=s[i]-'a';//注意这里的字符集大小默认为小写字母,根据题目要进行更改!!!
if(!tr[u][v])tr[u][v]=++cnt;//新建节点
u=tr[u][v];g[u].pb(id);
nm[u]++;//包含该节点 共几个字符串
flag[u]=i+1;//该节点是字符串的第几个节点(从前往后)
}
ed[id]=u;
}
void get_fail()
{
for(int i=0;i<26;i++)tr[0][i]=1;
q.push(1);fail[1]=0;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=0;i<26;i++)
{
int v=tr[u][i];//遍历u所有儿子,这样不同记录fa
int Fail=fail[u];//由于BFS遍历,fail[u]已经处理好了,现在是找v的fail
if(!v)tr[u][i]=tr[Fail][i];//不存在节点v,这样做的目的是,后面fail指针失配时直接不断返回fail
else fail[v]=tr[Fail][i],q.push(v);//存在实节点才入队列
}
}
}
ll gao(int u)
{
vector<int>tp;tp.clear();
ll ans=0;int k=u;
while(k>1)
{
for(auto x:g[k])if(!vs[x])vs[x]=1,ans=(ans+(ll)flag[k]*flag[k]%mod)%mod,tp.pb(x),sm++;
k=fail[k];
}
for(auto x:tp)vs[x]=0;
pr[u]=ans;
return ans;
}
}ac;
char s[M];
int main()
{
// freopen("12.in","r",stdin);
int n;
scanf("%d",&n);
ac.init();
for(int i=1;i<=n;i++)
{
scanf("%s",s);
ac.in(s,i);
}
ac.get_fail();
ll ans=0;
for(int i=1;i<=n;i++)
{
if(pr[ed[i]])ans=(ans+pr[ed[i]])%mod;//记忆化,防止复杂度退化为n^2
else ans=(ans+ac.gao(ed[i]))%mod;
}
printf("%lld\n",ans);
return 0;
}