LINK

对于特定的长度为 n n n的字符串,容易发现如果 [ l , r ] [l,r] [l,r]是一个禁忌字符串,那么答案马上可以加一

然后单独再对 [ r + 1 , n ] [r+1,n] [r+1,n]计算贡献,这样一定最优.

于是使用 a c ac ac自动机压缩一下 n n n个字符串的状态,定义 f [ i ] [ j ] f[i][j] f[i][j]表示前 i i i个字符匹配到 j j j位置的概率

每次枚举字母 k k k,设状态 j j j通过字母 k k k可以转移到状态 n x t nxt nxt

n x t nxt nxt为终止节点时,设 p r o a l p = 1 a l p h a b e t proalp=\frac{1}{alphabet} proalp=alphabet1

f [ i + 1 ] [ 0 ] + = f [ i ] [ j ] ∗ p r o a l p f[i+1][0]+=f[i][j]*proalp f[i+1][0]+=f[i][j]proalp

a n s + = f [ i ] [ j ] ans+=f[i][j] ans+=f[i][j]

n x t nxt nxt不为终止节点时

f [ i + 1 ] [ n x t ] + = f [ i ] [ j ] ∗ p r o a l p f[i+1][nxt]+=f[i][j]*proalp f[i+1][nxt]+=f[i][j]proalp

时间复杂度 O ( l e n ∗ t o t ) O(len*tot) O(lentot),其中 t o t tot tot表示自动机上的点数,大概在 200 200 200左右

代码LINK


注意到每次的转移是一样的,于是可以用矩阵来转移

初始矩阵为

S = [ f 0 f 1 ⋯ f t o t − 1 a n s ] S= \begin{bmatrix} f_0 & f_1 & \cdots & f_{tot-1} &ans\\ \end{bmatrix} S=[f0f1ftot1ans]

其中 f i f_i fi表示当前在自动机节点 i i i的概率, a n s ans ans表示现在的答案

转移矩阵是一个 ( t o t + 1 ) ∗ ( t o t + 1 ) (tot+1)*(tot+1) (tot+1)(tot+1)的矩阵 z z z

当状态 j j j能通过字母 k k k转移到状态 q q q z [ j ] [ q ] + = p r o a l p z[j][q]+=proalp z[j][q]+=proalp

特殊的,对于最后一列是计算答案用的,设状态 j j j能通过字母 k k k转移到终止节点 w w w

那么 z [ j ] [ t o t ] + = p r o a l p z[j][tot]+=proalp z[j][tot]+=proalp

z [ t o t ] [ t o t ] = 1 z[tot][tot]=1 z[tot][tot]=1,这是为了继承上一轮计算的 a n s ans ans

#include <bits/stdc++.h>
using namespace std;
#define Lf long double
const int maxn =3e5+10;
int zi[maxn][30],fail[maxn],id,val[maxn];
int n,len,alp,mx;
char s[maxn];
struct rce
{
	Lf a[109][109];
	rce(){ memset(a,0,sizeof a ); }
	rce operator * ( const rce&b )	const
	{
		rce s;
		for(int i=0;i<=mx;i++)
		for(int j=0;j<=mx;j++)
		{
			s.a[i][j] = 0;
			for(int k=0;k<=mx;k++)
				s.a[i][j] += a[i][k]*b.a[k][j];
		}
		return s;
	}
};
rce quick(rce a,int n)
{
	rce ans = a;
	for( ; n ; n>>=1,a = a*a )
		if( n&1 )	ans = ans*a;
	return ans;	
}
void insert(char s[])
{
	int le = strlen( s+1 ), now = 0;
	for(int i=1;i<=le;i++)
	{
		if( zi[now][s[i]-'a']==0 )	zi[now][s[i]-'a'] = ++id;
		now = zi[now][s[i]-'a'];	
	}
	val[now] = 1;//这个位置有字符串存在 
}
void make_fail()
{
	queue<int>q;
	for(int i=0;i<=25;i++)
		if( zi[0][i] )	q.push( zi[0][i] );
	while( !q.empty() )
	{
		int u = q.front(); q.pop();
		for(int i=0;i<=25;i++)
		{
			if( zi[u][i] )//存在这个节点
				fail[zi[u][i]] = zi[fail[u]][i],q.push( zi[u][i] );
			else
				zi[u][i] = zi[fail[u]][i]; 
		}
	}
}
int main()
{
	cin >> n >> len >> alp;
	for(int i=1;i<=n;i++)
	{
		scanf("%s",s+1 );
		insert( s );	
	}	
	make_fail(); mx = id+1;
	Lf proalp = 1.0/alp;
	rce chu, zhuan;
	chu.a[0][0] = 1;
	
	for(int q=0;q<=id;q++)//枚举状态 
	for(int j=0;j<alp;j++)
	{
		int nxt = zi[q][j];
		if( val[nxt] )
		{
			zhuan.a[q][0] += proalp;
			zhuan.a[q][id+1] += proalp;
		}
		else//不是终止状态 
			zhuan.a[q][nxt] += proalp;	 
	}
	zhuan.a[id+1][id+1] = 1;
	chu = chu*quick( zhuan,len-1 );
	printf("%.7Lf",chu.a[0][id+1] ); 
}