第一道AC自动机上的dp
题意是给出一些字符串,求长为m的字符串包含这些的一共有多少个,字符集A-Z
首先运用补集转换,转而求不含这些串的个数,最后用26^M减掉就行
根据输入的字符串建立AC自动机
dp[i][j]表示当前考虑了i位,当前停留在AC自动机的j号节点
每一次可以由dp[i][j]转移到dp[i+1][k],k是枚举第i+1为后作为j的儿子在AC自动机上的编号
枚举k,就是第i+1为填什么,然后进行下列操作:
首先看看这位能不能填k,判断方法是从j开始向fail[j]跳,看是不是有一个j有一个k儿子,并且k儿子上还有结束标记,只要有一个就证明如果i+1位填k就会让整个字符串出现AC自动机上的字符串,所以不能填k
如果能放,再看看要修改哪个dp数组。
还是从j开始向fail[j]跳,如果j有k这个儿子就直接修改dp[i+1][j的k儿子]就好
每次修改要对修改目标加上dp[i][j]
答案是所有dp[m][x](x是所有AC自动机上的节点)的和
代码
数组名称:
fail:失败指针
danger:结束标记
tr:trie树
q:队列
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#define md 10007
using namespace std;
char ch[1000];
int fail[6010],dp[1200][6010],f,ans;
int tr[6010][30],trcnt,danger[6010],i,j,n,m,q[6010],anss=1;
void insert() //建立trie树
{
int now=1,len=strlen(ch+1);
for (int k=1;k<=len;k++)
{
int y=ch[k]-'A'+1;
if (tr[now][y]) now=tr[now][y];
else now=tr[now][y]=++trcnt;
}
danger[now]=1;
}
void acmach() //跑出fail数组
{
int head=0,tail=0,now;
q[++tail]=1;
while (head<tail)
{
now=q[++head];
for (int k=1;k<=26;k++)
if (tr[now][k])
{
int y=fail[now];
while (!tr[y][k]) y=fail[y];
fail[tr[now][k]]=tr[y][k];
q[++tail]=tr[now][k];
}
}
}
int main()
{
scanf("%d %d",&n,&m);
for (i=1;i<=26;i++) tr[0][i]=1;
trcnt=1;
for (i=1;i<=n;i++)
{
scanf("%s",ch+1);
insert();
}
acmach();
dp[0][1]=1; //初始化
for (i=0;i<=m-1;i++)
for (j=1;j<=trcnt;j++)
{
for (int k=1;k<=26;k++)
{
int now=j;
f=0;
while (now) //这个循环判断k能不能放
{
if (danger[tr[now][k]]==1)
{
f=1;
break;
}
now=fail[now];
}
if (f==1) continue; //不能放直接跳过
now=j;
while (!tr[now][k]) now=fail[now]; //j向fail[j]跳直到有k儿子
now=tr[now][k];
dp[i+1][now]=(dp[i+1][now]+dp[i][j])%md; //修改
}
}
for (i=1;i<=m;i++)
{
anss=(anss*26)%md; //补集转换
}
for (i=1;i<=trcnt;i++)
{
ans=(ans+dp[m][i])%md; //最终答案是所有dp[m][x](x是所有AC自动机上的节点)的和
}
ans=(anss-ans+md)%md;
cout<<ans%md;
}