题解 P4569 【[BJWC2011]禁忌】

· · 题解

【算法分析】

算法一:(对于40\%的数据):

kmp上的dp+矩阵乘法优化,可通过子任务三,得40pts。

做法类似,GT考试

https://www.luogu.com.cn/problem/P3193

http://ybt.ssoier.cn:8088/problem_show.php?pid=1646

此处不详细讲了。

算法二(对于 30\% 的数据):

本题求的是禁忌伤害的期望值。实际上,期望也就是所有可能情况的禁忌伤害的的加权平均数。不难想到,可以枚举所有可能出现的字符串,所有串的禁忌伤害和除以串的个数即为答案。对于已知的一个串,计算它的禁忌伤害,显然可以贪心:我们需要把它分成若干段,每一段包含一个禁忌串,答案即为段数。对此,我们对该串求伤害的最大值,显然要把它划分成尽量多的段。那么我们可以对禁忌串建一个AC自动机,让已知串从根节点出发进行匹配,如果遇到一个终止节点(该节点表示的字符串或其后缀为某个完整的禁忌串),则这个串的禁忌伤害加1,回到根节点继续匹配。

复杂度O(\text{alphabet}^{\text{len}} \times len + \sum_{1}^{N}{|T_{i}|})可以通过子任务一,得30pts

算法三(对于 70\% 的数据):

在算法二贪心计算答案的启发下,我们发现可以使用AC自动机处理本题,考虑将枚举每一个串的搜索改为期望dp。不难发现,本题可以顺推。设\text{dp}_ {i,j}表示长度为i,匹配到AC自动机上的第j个节点的禁忌伤害概率值。

当$\text{ch}_ {j,k}$不是终止节点时,显然有$\text{dp}_{i,\text{ch}_{j,k}} + =\text{dp}_{i - 1,j} \times \frac{1}{\text{alphabet}}$的转移方程。 否则有$\text{dp}_{i,1} + = (\text{dp}_{i - 1,j} + 1) \times\frac{1}{\text{alphabet}}$的转移方程。 初始状态全部为0,答案为$\sum_{i = 1}^{\text{len}}\sum_{j = 1}^{\text{tot}} \text{dp}_{\text{i},j}\times\text{bo}_j$。 复杂度$O(len \times tot + \sum_{1}^{N}{|T_{i}|})$可通过子任务一、二和部分子任务三得70~90pts。 算法四: 算法三中,tot在100以下,几乎可以忽略不计,超时的原因显然是$10^{9}$级别的$len$。 我们可以发现:转移方程中只在相邻的长度上转移,显然是一个tot阶递推式,可以使用矩阵乘法优化!大体思想同算法一。但由于常数项的原因,我们的转移矩阵应该是$(tot + 1) \times (tot + 1)$的。$a_{i,tot + 1}$表示当前状态到点i已经计入答案的期望值。初始矩阵,只需设为单位矩阵即可,转移矩阵构造方法如下:枚举AC自动机上每一个节点, 设当前到了第i个节点,则枚举i的每一条出边,对于点i的第k条出边 ($0 \leq k < alphabet$),若$\text{ch}_{i,k}$是终止节点,则$a_{i,tot +1} + = \frac{1}{\text{alphabet}},a_{i,tot + 1} + =\frac{1}{\text{alphabet}}$. 否则$a_{i,\text{ch}_{i,k}} + = \frac{1}{\text{alphabet}}$,特别地$a_{tot+ 1,tot + 1} = 1$保存过程中的答案,最终取$a^{\text{len}}$矩阵。 答案为$a_{1,tot + 1}$(转移len次后根节点对答案的贡献)。 使用矩阵快速幂优化,复杂度$O(\text{tot}^{3} \times\operatorname{log_2}\text{len} + tot \times \text{alphabet} +\sum_{1}^{N}{|T_{i}\ |})$,得100pts。 注意事项: 代码实现中建议全程使用long double,其他细节见代码注释。 【参考程序】 ```cpp #include<bits/stdc++.h> #define ll long long #define re register #define al alphabet #define ld long double//本题精度要求较高,建议使用long double型变量 using namespace std; inline int read() { int lzx=0,f=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();} while(c>='0'&&c<='9'){lzx=lzx*10+c-'0';c=getchar();} return lzx*f; } int ch[80][26],net[80],bo[80]; int n,len,al,tot=1; char s[101]; inline void add()//建Trie树 { int len=strlen(s+1),u=1; for(re int i=1;i<=len;i++) { int c=s[i]-'a'; if(!ch[u][c])ch[u][c]=++tot; u=ch[u][c]; } bo[u]=1;//点u表示的串是禁忌串 return; } inline void bfs()//求next数组,建AC自动机 { for(re int i=0;i<26;i++) ch[0][i]=1; net[1]=0; queue<int> q; q.push(1); while(!q.empty()) { int u=q.front(); bo[u]|=bo[net[u]];//它的后缀为禁忌串,它也是禁忌串 for(re int i=0;i<26;i++) { int v=net[u]; while(!ch[v][i])v=net[v]; if(ch[u][i]) { q.push(ch[u][i]); net[ch[u][i]]=ch[v][i]; } else ch[u][i]=ch[v][i]; } q.pop(); } return; } struct point { ld mapp[110][110]; int a,b; };//用结构体保存矩阵信息 inline void pre(point &a) { for(re int i=1;i<=a.a;i++) for(re int j=1;j<=a.b;j++) a.mapp[i][j]=0.0; for(re int i=1;i<=tot;i++) { for(re int j=0;j<al;j++) { if(bo[ch[i][j]]) { a.mapp[i][1]+=(ld)1.0/(ld)al; a.mapp[i][tot+1]+=(ld)1.0/(ld)al; } else a.mapp[i][ch[i][j]]+=(ld)1.0/(ld)al; } } return; }//构造转移矩阵 inline point times(point a,point b) { point c;c.a=c.b=a.a; for(re int i=1;i<=c.a;i++) for(re int j=1;j<=c.b;j++) c.mapp[i][j]=(ld)0.0; for(re int i=1;i<=c.a;i++) for(re int j=1;j<=c.b;j++) for(re int k=1;k<=a.b;k++) c.mapp[i][j]+=a.mapp[i][k]*b.mapp[k][j]; return c; }//两个矩阵相乘 inline point ksm(point a,int k) { point res;res.a=res.b=a.a; for(re int i=1;i<=res.a;i++) for(re int j=1;j<=res.b;j++) res.mapp[i][j]=(ld)(i==j); while(k) { if(k&1)res=times(res,a); a=times(a,a); k>>=1; } return res; }//矩阵快速幂 point a; int main() { n=read(),len=read(),al=read(); for(re int i=1;i<=n;i++) { scanf("%s",s+1); add(); } bfs(); a.a=a.b=tot+1;//矩阵规格为(tot+1)×(tot+1) pre(a); a.mapp[a.a][a.b]=(ld)1.0; a=ksm(a,len);//求矩阵a^len printf("%Lf\n",a.mapp[1][tot+1]);//long double型用“%Lf”输出 return 0; } ```