概率dp
概率dp
引入
概率dp用于解决概率问题与期望问题,一般情况下,解决概率问题需要顺序循环,而解决期望问题使用逆序循环,如果定义的状态转移方程存在后效性问题,还需要用到高斯消元来优化。概率dp也会结合其他知识进行考察,例如状态压缩,树上进行 DP 转移等。
dp求概率
这类题目采用顺推,也就是从初始状态推向结果。同一般的dp类似的,难点依然是对状态转移方程的刻画,只是这类题目经过了概率论知识的包装。
例题 Bag of mice
题目大意:袋子里有 \(w\) 只白鼠和 \(b\) 只黑鼠,公主和龙轮流从袋子里抓老鼠。谁先抓到白色老鼠谁就赢,如果袋子里没有老鼠了并且没有谁抓到白色老鼠,那么算龙赢。公主每次抓一只老鼠,龙每次抓完一只老鼠之后会有一只老鼠跑出来。每次抓的老鼠和跑出来的老鼠都是随机的。公主先抓。问公主赢的概率。
过程
设 \(f_{i,j}\) 为轮到公主时袋子里有 \(i\) 只白鼠,\(j\) 只黑鼠,公主赢的概率。初始化边界,\(f_{0,j}=0\) 因为没有白鼠了算龙赢,\(f_{i,0}=1\) 因为抓一只就是白鼠,公主赢。 考虑 \(f_{i,j}\) 的转移:
- 公主抓到一只白鼠,公主赢了。概率为 \(\frac{i}{i+j}\);
- 公主抓到一只黑鼠,龙抓到一只白鼠,龙赢了。概率为 \(\frac{j}{i+j}\cdot \frac{i}{i+j-1}\);
- 公主抓到一只黑鼠,龙抓到一只黑鼠,跑出来一只黑鼠,转移到 \(f_{i,j-3}\)。概率为 \(\frac{j}{i+j}\cdot\frac{j-1}{i+j-1}\cdot\frac{j-2}{i+j-2}\);
- 公主抓到一只黑鼠,龙抓到一只黑鼠,跑出来一只白鼠,转移到 \(f_{i-1,j-2}\)。概率为 \(\frac{j}{i+j}\cdot\frac{j-1}{i+j-1}\cdot\frac{i}{i+j-2}\);
考虑公主赢的概率,第二种情况不参与计算。并且要保证后两种情况合法,所以还要判断 \(i,j\) 的大小,满足第三种情况至少要有 \(3\) 只黑鼠,满足第四种情况要有 \(1\) 只白鼠和 \(2\) 只黑鼠。
实现
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1010;
int w, b;
double dp[kMaxN][kMaxN];
int main() {
cin >> w >> b;
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= w; i++) dp[i][0] = 1; // 初始化
for (int i = 1; i <= b; i++) dp[0][i] = 0;
for (int i = 1; i <= w; i++) {
for (int j = 1; j <= b; j++) { // 以下为题面概率转移
dp[i][j] += (double)i / (i + j);
if (j >= 3) {
dp[i][j] += (double)j / (i + j) * (j - 1) / (i + j - 1) * (j - 2) / (i + j - 2) * dp[i][j - 3];
}
if (i >= 1 && j >= 2) {
dp[i][j] += (double)j / (i + j) * (j - 1) / (i + j - 1) * i / (i + j - 2) * dp[i - 1][j - 2];
}
}
}
printf("%.9lf\n", dp[w][b]);
return 0;
}
dp求期望
例题1 Collecting Bugs
题目大意
一个软件有 \(s\) 个子系统,会产生 \(n\) 种 \(bug\)。某人一天发现一个 \(bug\),这个 \(bug\) 属于某种 \(bug\) 分类,也属于某个子系统。每个 \(bug\) 属于某个子系统的概率是 \(\frac{1}{s}\),属于某种 \(bug\) 分类的概率是 \(\frac{1}{n}\)。求发现 \(n\) 种 \(bug\),且 \(s\) 个子系统都找到 \(bug\) 的期望天数。
过程
令 \(f_{i,j}\) 为已经找到 \(i\) 种 \(bug\) 分类,\(j\) 个子系统的 \(bug\),达到目标状态的期望天数。这里的目标状态是找到 \(n\) 种 \(bug\) 分类,\(s\) 个子系统的 \(bug\)。那么就有 \(f_{n,s}=0\),因为已经达到了目标状态,不需要用更多的天数去发现 \(bug\) 了,于是就以目标状态为起点开始递推,答案是 \(f_{0,0}\)。
考虑 \(f_{i,j}\) 的状态转移:
- \(f_{i,j}\),发现一个 \(bug\) 属于已经发现的 \(i\) 种 \(bug\) 分类,\(j\) 个子系统,概率为 \(p_1=\frac{i}{n}\cdot\frac{j}{s}\)
- \(f_{i,j+1}\),发现一个 \(bug\) 属于已经发现的 \(i\) 种 \(bug\) 分类,不属于已经发现的子系统,概率为 \(p_2=\frac{i}{n}\cdot(1-\frac{j}{s})\)
- \(f_{i+1,j}\),发现一个 \(bug\) 不属于已经发现 \(bug\) 分类,属于 \(j\) 个子系统,概率为 \(p_3=(1-\frac{i}{n})\cdot\frac{j}{s}\)
- \(f_{i+1,j+1}\),发现一个 \(bug\) 不属于已经发现 \(bug\) 分类,不属于已经发现的子系统,概率为 \(p_4=(1-\frac{i}{n})\cdot(1-\frac{j}{s})\)
再根据期望的线性性质,就可以得到状态转移方程:
\(\begin{aligned} f_{i,j} &= p_1\cdot f_{i,j}+p_2\cdot f_{i,j+1}+p_3\cdot f_{i+1,j}+p_4\cdot f_{i+1,j+1} + 1\\ &= \frac{p_2\cdot f_{i,j+1}+p_3\cdot f_{i+1,j}+p_4\cdot f_{i+1,j+1}+1}{1-p_1} \end{aligned}\)
实现
#include <cstdio>
using namespace std;
const int kMaxN = 1010;
int n, s;
double dp[kMaxN][kMaxN];
int main() {
scanf("%d %d", &n, &s);
dp[n][s] = 0;
for (int i = n; i >= 0; i--) {
for (int j = s; j >= 0; j--) {
if (i == n && s == j) continue;
dp[i][j] = (dp[i][j + 1] * i * (s - j) + dp[i + 1][j] * (n - i) * j + dp[i + 1][j + 1] * (n - i) * (s - j) + n * s) / (n * s - i * j); // 概率转移
}
}
printf("%.4lf\n", dp[0][0]);
return 0;
}
例题2 飞行棋
题目大意
有一架飞机一开始在 \(n\) 号位置。牛牛每回合可以先投掷一次 \(d\) 面的骰子,\(1\) 到 \(d\) 等概率出现。投出点数 \(x\) 后,飞机会移动 \(x\) 步,每步移动一格,方向初始向左移动,若到达终点,会向右移动。若投出的点数为 \(d\) 点,可以继续投掷,直到投出的点数不是 \(d\) 点。求让这架飞机停在 \(0\) 号位置回合数的期望。
过程
设 \(dp_x\) 为从 \(x\) 走到 \(1\) 的步数。
- 当 \(x\geq d\) 时,\(dp_x=\sum_{i=1}^d\frac{dp_x-1}{d}\);
- 当 \(X<D\)时,期望为 \(d-1\)。
实现
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1e5 + 10;
int t, n, d;
double dp[kMaxN], s;
int main() {
for (cin >> t; t--;) {
cin >> n >> d;
dp[0] = 1, s = 0;
for (int i = 1; i < d; i++) {
dp[i] = d - 1;
s += dp[i] + 1;
}
for (int i = d; i <= n; i++) {
dp[i] = dp[i - d] / d + s / d;
s = s - dp[i - d + 1] + dp[i];
}
printf("%.2lf\n", dp[n]);
}
return 0;
}