概率與期望
感謝 FSYo巨佬的博客
前置內容
- 若有一次操作中事件 A 發生的概率為 \(P(A)\),那么要事件 A 發生,則期望 \(\dfrac1{P(A)}\) 次操作
例:求拋一個骰子拋到 4 ,期望拋多少次?
因為 \(P(拋到4)=\dfrac16\) ,所以答案就是 6 次。
或者這么想:
設 \(P_k\) 為拋第 k 次才得到 4 的概率,也就是前 \(k-1\) 次均不是 4 ,那么 \(P_k=(\dfrac56)^{k-1}\times\dfrac16\)
所以 \(Ans=\sum\limits_{k=1}^\infty P_k\times k=\dfrac16\sum\limits_{k=1}^\infty (\dfrac56)^{k-1}\times k\)
接下來有兩種做法
- 交換求和號 + 等比數列
\[Ans=\frac16\sum\limits_{k=1}^\infty (\frac56)^{k-1}\times k =\frac16\sum\limits_{k=1}^\infty\sum_{i=1}^k(\frac56)^{k-1} =\frac16\sum_{i=1}^\infty\sum_{k=i}^\infty(\frac56)^{k-1}\\ Ans=\frac16\sum_{i=1}^\infty \lim_{n\to\infty}((\frac56)^{i-1}\times\frac{1-(\frac56)^n}{1-\frac56}) =\sum_{i=1}^\infty(\frac56)^{i-1} =\sum_{i=0}^\infty(\frac56)^{i}\\ Ans=\lim_{n\to\infty}(1\times\frac{1-(\frac56)^n}{1-\frac56})=6 \]
- 生成函數
\[Ans=\frac16\sum\limits_{k=1}^\infty (\frac56)^{k-1}\times k =\frac16\sum\limits_{k=0}^\infty (\frac56)^k\times(k+1) \]令 \(x=\dfrac56\) ,那么\[Ans=\frac16\sum\limits_{k=0}^\infty (k+1)\ x^k =\frac16\times(1+2x+3x^2+4x^3+\cdots)\\ Ans=\frac16\times(1+x+x^2+x^3+\cdots)^2 =\frac16\times(\frac1{1-x})^2\\ Ans=\frac16\times\frac1{(1-\frac56)^2}=6 \]
例題
[Luogu UVA10529] Dumb Bones
概率 + 最優,考慮一個區間DP。
枚舉這個區間里最后擺放的一塊骨牌的位置,把原區間分割成左右兩個子區間。
設原區間期望最少擺放次數為 \(E\) ,左區間為 \(E_1\) ,右區間 \(E_2\) 。此時最后一塊骨牌還沒有放上去,那么設這個空位的位置是 \(x\)
已知每塊骨牌向左倒的概率是 \(P_l\) ,向右是 \(P_r\)
單獨看 \(x\) 這一個骨牌,它不倒的概率是 \(1-P_l-P_r\) ,那么使它不倒的期望擺放次數是 \(\dfrac1{1-P_l-P_r}\)
注意這個次數里:最后一次是不倒的,之前的有些向左有些向右。因為向左的概率是 \(P_l\) ,所以向左倒的次數就是 \(\dfrac1{1-P_l-P_r}\times P_l\) 。由於每向左倒一次,左區間的就要全部重新擺放。左區間放好一次需要 \(E_1\) 個擺放,所以一共左區間要擺 \(\dfrac{P_l}{1-P_l-P_r}\times E_1\) 次。同理右區間一共要放置 \(\dfrac{P_r}{1-P_l-P_r}\times E_2\) 次。
還有放 \(x\) 之前需要先把左右區間都擺好一次,共 \(E_1+E_2\) 個擺放。
狀態轉移方程
因為每塊牌都一模一樣,顯然對於所有長度相同的區間,它們的 \(E\) 都是相同的
把狀態設置為區間長度,就只有不同長度的區間才需要轉移
#include <cstdio>
#include <iostream>
using namespace std;
double pl, pr;//向左、向右倒的概率
double f[1030];
double dfs(int n) {//長度為 n 的區間
double &E = f[n], E1, E2;//還是引用
if (E > -0.5) return E;//讓 f[0]=0.0 和 f[1]=1.0/(1.0-pl-pr) 都可以自然轉移
E = 1e20;
for (int i=1; i<=n; ++i) {
E1 = dfs(i-1), E2 = dfs(n-i);//左、右區間的答案
E = min(E, E1 + E2 + f[1] + pl*E1*f[1] + pr*E2*f[1]);
} return E;
}
signed main() {
ios::sync_with_stdio(false);cin.tie(nullptr);
register int n;
while (cin>>n && n) {
cin >> pl >> pr;
for (int i=2; i<=n; ++i) f[i] = -1.0;//注意細節
f[1] = 1.0 / (1.0 - pl - pr);//同時 f[0]=0
printf("%.2f\n", dfs(n));
} return 0;
}
[WOJ2276] 挑戰
設 \(f_{i,j,t}\) 表示,前 i 次中,成功了 j 次,剩余 t 個單位的包包空間,的概率。
觀察發現最多有 n 個地圖碎片,也就是最多只需要 n 個單位的包包空間,所以超過 n 的包包空間的情況都可以合並到 n 里去。這樣就不會爆空間了。
注意下標 t 為負的情況。
#include <iostream>
#include <iomanip>
using namespace std;
#define N 230
int n, L, k, a[N];
double p[N], f[N][N][N<<1];
signed main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> n >> L >> k;
for (int i=1; i<=n; ++i) cin >> p[i], p[i]/= 100.0;
for (int i=1; i<=n; ++i) cin >> a[i];
if (k > n) k = n;
f[0][0][N+k] = 1.0;//易得邊界值
for (int i=1; i<=n; ++i) {//枚舉第 i 次
for (int j=0; j<i; ++j) {//枚舉第 1 ~ i-1 次中共成功了 j 次
int l = N+k-i, r = N+n;//允許的背包容量范圍
for (int t=l; t<=r; ++t) {//枚舉當前背包容量 t
f[i][j][t] += f[i-1][j][t] * (1.0 - p[i]);//這一次沒成功
f[i][j+1][min(N+n, t+a[i])] += f[i-1][j][t] * p[i];//這一次成功了
}
}
}
double ans = 0;
for (int j=L; j<=n; ++j)
for (int t=0; t<=n; ++t)
ans += f[n][j][N+t];
cout << fixed << setprecision(6) << ans; return 0;
}
[Luogu P4550] 收集郵票
設兩個 DP 數組: \(f_i,g_i\) ,表示已經獲得 \(i\) 張不同郵票后還需買 \(f_i\) 張郵票才能湊齊 \(n\) 張不同郵票,這 \(f_i\) 張郵票要花 \(g_i\) 元
根據這兩個的定義,我們選擇倒序 DP
根據“前置內容”中的定理,易得
因為每次不選到已經選過的前 \(i\) 張的概率是 \(\dfrac{n-i}n\) 。
考慮我們買的單價是倒序的,跟 DP 順序一致。最后買的郵票 1 元,倒數第 2 張 2 元 … 第 1 張郵票 \(f_0\) 元。
假設現在已經擁有 \(i\) 種不同的郵票,一共有 \(k\) 張。想一想買第 \(k+1\) 張的情況。
有 \(\dfrac in\) 的概率與前 \(k\) 張重復,那就還是只有 \(i\) 種郵票,之后就還要買 \(f_i\) 張共 \(g_i\) 元才能湊齊 \(n\) 種。所以第 \(k+1\) 張就是倒數第 \(f_i+1\) 張,花費 \(f_i+1\) 元。從第 \(k+1\) 張開始一直到最后一共就要花 \(g_i+f_i+1\) 元。
有 \(\dfrac {n-i}n\) 的概率是新郵票,就有 \(i+1\) 種郵票了,之后就還要買 \(f_{i+1}\) 張共 \(g_{i+1}\) 元才能湊齊 \(n\) 種。那么第 \(k+1\) 張就是倒數第 \(f_{i+1}+1\) 張,花費 \(f_{i+1}+1\) 元。從第 \(k+1\) 張開始一直到最后一共就要花 \(g_{i+1}+f_{i+1}+1\) 元。
因為 \(g_i\) 表示從第 \(k\) 張開始一直到最后花的錢,那么得到方程
化簡一下就行了
#include <stdio.h>
#define N 10030
double f[N], g[N], n;//這里要注意一下精度問題,我們把 n 設為 double
signed main() {
scanf("%lf", &n);//讀成 double
for (int i=n-1; ~i; --i) {//這里易證邊界:f[n]=g[n]=0,就不多加贅述了
f[i] = f[i+1] + n/(n-i);
g[i] = g[i+1] + f[i+1] + i*f[i]/(n-i) + n/(n-i);//為了精度,改變一下乘除法順序
} printf("%.2f", g[0]);
return 0;
}//發現數組每次只用了兩位,所以程序還可以進一步優化
[WOJ3083] 收集寶石
與上題類似
設 \(f_{i,j}\) 表示已經有 \(i\) 種寶石、 \(j\) 個不同的下水道后,還需要的期望天數,倒序 DP。
假設現在已經過去 \(k\) 天(正序),對於第 \(k+1\) 天,則
有 \(\dfrac{ij}{ns}\) 的概率轉移到 \(f_{i,j}\)
有 \(\dfrac{(n-i)j}{ns}\) 的概率轉移到 \(f_{i+1,j}\)
有 \(\dfrac{i(s-j)}{ns}\) 的概率轉移到 \(f_{i,j+1}\)
有 \(\dfrac{(n-i)(s-j)}{ns}\) 的概率轉移到 \(f_{i+1,j+1}\)
得到方程
方程中的 +1 是指第 \(k+1\) 天當天,如果還不能理解方程請回看上一題
化簡即可
#include <stdio.h>
double f[1003][1003], n, s;
signed main() {
scanf("%lf%lf", &n, &s);
for (int i=n; ~i; --i)
for (int j=s; ~j; --j) {
if (i==n && j==s) continue;//存在邊界:f[n][s]=0
f[i][j] = (
n*s
+ (n-i)*j * f[i+1][j]
+ i*(s-j) * f[i][j+1]
+ (n-i)*(s-j) * f[i+1][j+1]
) / (n*s - i*j);
} printf("%.4f", f[0][0]); return 0;
}
[SCOI2008] 獎勵關
狀壓 DP
設 \(f_{x,s}\) 表示已經走了 \(x\) 次,前 \(x\) 次收集寶物的狀態是 \(s\) ,第 \(x+1\) 到 第 \(k\) 輪的最大期望得分。
還是一樣地倒着 DP。
枚舉所有寶物 \(i\) ,每個 \(i\) 的概率是 \(\dfrac1n\) ,計算期望時就可以先把和算出來最后取平均值。
如果當前狀態可以吃,則分為吃與不吃兩種狀態,取較大值為優;如果不能吃,就直接轉移。
#include <stdio.h>
#include <algorithm>
int n, k;
int p[18], s[18];
double f[103][70000];
double dfs(int x, int state) {//這里我采取了記憶化搜索的寫法,一般的for循環遞推也是可以的
if (x >= k) return 0;//易證邊界f[k][...]=0
double &ret = f[x][state];//引用
if (ret > -0.5) return ret;//記憶化
ret = 0;
for (int i=1; i<=n; ++i) {
if ((s[i] & state) == s[i]) {//如果可以吃i,就有吃與不吃兩種狀態
ret += std::max(dfs(x+1, state|(1<<(i-1))) + p[i], dfs(x+1, state));
//這里其實是退棧的時候計算答案,所以本質就是倒序DP的
} else ret += dfs(x+1, state);//如果不能吃i
} return ret /= n;//取平均值(期望值)
}
signed main() {
scanf("%d%d", &k, &n);
for (int i=1, tmp; i<=n; ++i) {
scanf("%d", p+i);
while (true) {
scanf("%d", &tmp);
if (!tmp) break;
s[i] |= 1 << (tmp - 1);
}
}
for (int i=0; i<=k; ++i)
for (int j=0; j<=(1<<15); ++j)
f[i][j] = -1;//初始化
printf("%.6f", dfs(0, 0)); return 0;
}