SDOI2017 硬币游戏
题意
有 \(n\) 个人,第 \(k\) 个人猜一个长度为 \(m\) 的01串 \(A_k\),互不相同;接下来依次生成一列随机变量 \(\{X_i\}\),其中每一个等概率为0或1。当 \(A_k\) 在这一列随机变量中连续出现时 \(k\) 胜利,并且其他人不再胜利。问每个人胜利的概率 \(\{P_k\}\)。(\(1\le n,m\le 300\))
题解
有一个朴素做法。建出AC自动机,在每一个时刻,随着随机变量的产生,状态会进行一系列转移,停留在一个特定节点。所以在自动机上依据转移边构造一系列方程,其中每个变量表示某个特定节点被经过次数的期望。
注意这一步转化很重要,因为在状态转移过程中会不止一次经过一个节点。设想,如果在大多情况会多次经过一个节点 \(a\),但是往往只经过一次节点 \(b\),然而两者“被经过”的概率相同,那么假设终止状态 \(x\) 从 \(a\) 转移,\(y\) 从 \(b\) 转移,则转移到 \(x\) 的概率显然比 \(y\) 更高。然而,如果只记录转移过程中“是否经过”某个节点,就不能体现多次经过的贡献。而经过次数的数学期望,就体现了转移次数这个重要信息。
那么怎样体现胜利概率呢?很简单,只要不允许终止状态产生任何后续转移,那么每种情况只会有唯一的一个终止状态被经过唯一一次,符合题意,而终止节点被经过次数的期望就是胜利概率。高斯消元即可。这道题是《JSOI2009 有趣的游戏》。
然而这题数据范围太大了,高斯消元 \(O(n^3m^3)\) 不能接受。当遇到这种情况时,往往要考虑压缩状态,忽略冗余信息的方法。这道题需要的仅仅是每个人的终止节点的次数期望,这些变量必须保留,而前面那些状态只要转移到最终状态就行了,至于如何转移,从哪里转移,互相怎么转移,这些信息应该试图去简化、合并。设到达一个状态 \(S\) 的次数为随机变量 \(Tot(S)\),第 \(k\) 个人赢的概率(也就是它的状态到达次数)为 \(f(k)\)。
如果做过《CTSC2006 歌唱王国》(我也写过题解)那么对于它的生成函数做法一定记忆犹新(虽然洛谷第一篇用离散时间鞅的做法也很巧妙)。这里也考虑这种思路。考虑当前正停留在任意一个没有结束的状态 \(Y\),接下来发生了一个特殊事件 \(V_k\),也就是接下来随机出的长度为 \(m\) 的序列恰好就是 \(A_k\)。注意这时可能已经终止,仍然无条件继续生成序列,但是正如上面所说,胜利以后状态不转移,也不继续累计到达次数。 正如《歌唱王国》的处理思路,事件 \(V_k\) 发生次数的期望有两种计算方法,第一种(这是显然的):
第二种则是考虑,根据 \(V_k\) 的定义,设有人胜利的时间是随机变量 \(T\),则 \(V_k\) 发生的条件下,有 \(T\in [|Y|+1, |Y|+m]\),胜利者一定已经产生(不一定是 \(k\)),所以考虑枚举 \(T\) 的取值为 \(|Y|+t\) 和获胜的人 \(p\),然后让胜利以后继续生成出剩下的状态,于是得到:
结合第一个式子,我们就能得到一个 \(n\) 个方程。其中唯一不知道的量就是 \(\left(\sum\limits_{Y}E\left(Y\right)\right)\),直接设它为一个未知量,再结合游戏必定终止,得到 \(\sum\limits_{k=1}^n f_k = 1\),总共 \(n+1\) 个方程和未知量,高斯消元即可。
代码
// Author: kyEEcccccc
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using ULL = unsigned long long;
int t0 = clock();
const int N = 305, PP = 131;
int n, m;
ULL hsh[N][N], rhsh[N][N], kp[N];
double pw2[N], mat[N][N];
int main(void)
{
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(nullptr);
pw2[0] = 1; kp[0] = 1;
for (int i = 1; i <= 300; ++i)
pw2[i] = pw2[i-1] / 2, kp[i] = kp[i-1] * PP;
cin >> n >> m;
for (int i = 0; i < n; ++i)
{
string s;
cin >> s;
for (int j = 1; j <= m; ++j)
hsh[i][j] = hsh[i][j-1] * PP + (s[j-1] == 'H' ? 3 : 5);
for (int j = m; j >= 1; --j)
rhsh[i][j] = rhsh[i][j+1] + kp[m-j] * (s[j-1] == 'H' ? 3 : 5);
}
for (int i = 0; i <= n; ++i)
for (int j = 0; j <= n+1; ++j)
mat[i][j] = 0;
for (int i = 0; i < n; ++i)
mat[n][i] = 1;
mat[n][n+1] = 1;
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < n; ++j)
for (int k = 1; k <= m; ++k)
if (hsh[i][k] == rhsh[j][m-k+1])
mat[i][j] += pw2[m-k];
mat[i][n] += -pw2[m];
}
// for (int i = 0; i < n; ++i)
// for (int j = 0; j <= n+1; ++j)
// cout << mat[i][j] << " \n"[j==n+1];
for (int i = 0; i <= n; ++i)
{
double t = mat[i][i];
for (int j = 0; j <= n+1; ++j)
mat[i][j] /= t;
for (int j = 0; j <= n; ++j)
{
if (j == i) continue;
double d = mat[j][i];
for (int k = 0; k <= n+1; ++k)
mat[j][k] -= mat[i][k] * d;
}
}
// for (int i = 0; i <= n; ++i)
// for (int j = 0; j <= n+1; ++j)
// cout << mat[i][j] << " \n"[j==n+1];
for (int i = 0; i < n; ++i)
cout << fixed << setprecision(10) << mat[i][n+1] << '\n';
// cerr << double(clock() - t0) / CLOCKS_PER_SEC << '\n';
return 0;
}