1. 程式人生 > 其它 >SDOI2017 硬幣遊戲

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 有趣的遊戲》。

然而這題資料範圍太大了,高斯消元\(\Omicron(n^3m^3)\)

不能接受。當遇到這種情況時,往往要考慮壓縮狀態,忽略冗餘資訊的方法。這道題需要的僅僅是每個人的終止節點的次數期望,這些變數必須保留,而前面那些狀態只要轉移到最終狀態就行了,至於如何轉移,從哪裡轉移,互相怎麼轉移,這些資訊應該試圖去簡化、合併。設到達一個狀態\(S\)的次數為隨機變數\(Tot(S)\),第\(k\)個人贏的概率(也就是它的狀態到達次數)為\(f(k)\)

如果做過《CTSC2006 歌唱王國》(我也寫過題解)那麼對於它的生成函式做法一定記憶猶新(雖然洛谷第一篇用離散時間鞅的做法也很巧妙)。這裡也考慮這種思路。考慮當前正停留在任意一個沒有結束的狀態\(Y\),接下來發生了一個特殊事件\(V_k\)

,也就是接下來隨機出的長度為\(m\)的序列恰好就是\(A_k\)注意這時可能已經終止,仍然無條件繼續生成序列,但是正如上面所說,勝利以後狀態不轉移,也不繼續累計到達次數。 正如《歌唱王國》的處理思路,事件\(V_k\)發生次數的期望有兩種計算方法,第一種(這是顯然的):

\[E(Tot(V_k)) = \left(\sum\limits_{Y}E\left(Y\right)\right)\cdot 2^{-m} \]

第二種則是考慮,根據\(V_k\)的定義,設有人勝利的時間是隨機變數\(T\),則\(V_k\)發生的條件下,有\(T\in [|Y|+1, |Y|+m]\),勝利者一定已經產生(不一定是\(k\)),所以考慮列舉\(T\)的取值為\(|Y|+t\)和獲勝的人\(p\),然後讓勝利以後繼續生成出剩下的狀態,於是得到:

\[E(Tot(V_k)) = \sum\limits_{p=1}^{n}\sum\limits_{t=1}^{m}[Pre_t(A_k) = Suf_t(A_p)]2^{-(m-t)}f(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;
}