大致题意: 给定(n)个禁忌串,随机生成一个长度为(m)、由前(alphabet)种小写字母构成的字符串,把它划分成若干段并最大化其中是禁忌串的段数,求这个段数的期望。
(AC)自动机
这种多模匹配问题,一看就要先建个(AC)自动机出来。
然后我们发现,最大化段数显然就是在一出现禁忌串时就将答案加(1),并返回根节点。
那么我们只要在建(AC)自动机时预处理出每个节点是否以某个禁忌串为后缀,就可以开心地在(AC)自动机上(DP)了。
我们设(f_{i,j})表示从根节点出发第(i)步走到(j)节点的概率,显然对于每个节点只需要枚举其后继状态就可以得出转移:
- 若该后继状态以某个禁忌串为后缀,转移到根节点,并将答案加上这一概率。
- 否则,直接转移到该后继状态。
转移系数就是(frac1{alphabet})。
然后我们发现长度(m)很大,于是自然而然想到矩乘。
但如何统计答案呢?
可以发现答案只会在返回根节点时更新,因此只要注意增开第(0)行/列统计到根节点的概率总和即可。
具体实现详见代码。
代码
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100
#define DB long double
using namespace std;
int n,m,Mx,Nt;char s[N+5];
struct M
{
DB a[N+5][N+5];I M() {memset(a,0,sizeof(a));}I DB *operator [] (CI x) {return a[x];}
I M operator * (Con M& o) Con//矩乘
{
M t;RI i,j,k;for(i=0;i<=Nt;++i) for(j=0;j<=Nt;++j)
for(k=0;k<=Nt;++k) t[i][j]+=a[i][k]*o.a[k][j];return t;
}
I M operator ^ (RI y) Con//矩阵快速幂
{
M x=*this,t;for(RI i=0;i<=Nt;++i) t[i][i]=1;W(y) y&1&&(t=t*x,0),x=x*x,y>>=1;return t;
}
}U;
class AcAutomation//AC自动机
{
private:
int q[N+5],vis[N+5];struct node {int V,F,S[30];}O[N+5];
public:
I void Ins(char *s)//插入字符串
{
RI i,x=1,t,l=strlen(s+1);for(i=1;i<=l;++i)
!O[x].S[t=s[i]&31]&&(O[x].S[t]=++Nt),x=O[x].S[t];O[x].V=1;
}
I void Build()//建AC自动机
{
RI i,k,H=1,T=0;for(i=1;i<=Mx;++i) (O[1].S[i]?O[q[++T]=O[1].S[i]].F:O[1].S[i])=1;
W(H<=T) for(k=q[H++],i=1;i<=Mx;++i) O[k].S[i]?//同时维护是否以某禁忌串为后缀
O[q[++T]=O[k].S[i]].F=O[O[k].F].S[i],O[O[k].S[i]].V|=O[k].V:O[k].S[i]=O[O[k].F].S[i];
}
I void GetU(CI x=1)//得出转移矩阵
{
if(vis[x]) return;for(RI i=vis[x]=1;i<=Mx;++i) O[O[x].S[i]].V?//枚举后继状态
(U[x][1]+=1.0L/Mx,U[x][0]+=1.0L/Mx):(U[x][O[x].S[i]]+=1.0L/Mx,GetU(O[x].S[i]),0);//分两种情况讨论
}
}AC;
int main()
{
RI i;for(Nt=1,scanf("%d%d%d",&n,&m,&Mx),i=1;i<=n;++i) scanf("%s",s+1),AC.Ins(s);
return AC.Build(),AC.GetU(),U[0][0]=1,printf("%.10Lf",(U^m)[1][0]),0;//矩乘输出答案
}