概率期望
概率
概率:随机事件发生的可能性,是一个 \(0\) 到 \(1\) 之间的实数。
古典概型:\(P(A)= \frac{ A \mbox{ 发生的情况数 } }{\mbox{ 总情况数 }}\)
古典概型的特点:
- 有限性:所有可能出现的基本事件只有有限个
- 等可能性:每个基本事件出现的可能性相等
如投掷一个 \(6\) 面的质量分布均匀的骰子,数字 \(1 \sim 6\) 朝上的概率均为 \(1/6\),奇数朝上的概率为 \(1/2\),偶数朝上的概率为 \(1/2\)。
计算概率:分类用加法原理,分步用乘法原理。
概率加法:两个不会同时发生的事件 \(A,B\) 的并集发生的概率 \(P(A+B)=P(A)+P(B)\),如果 \(A\) 和 \(B\) 可能同时发生,需要容斥,即再减去 \(AB\) 同时发生的概率 \(P(AB)\)。
概率乘法:
- 独立事件:两个互相独立的事件 \(A,B\) 同时发生的概率 \(P(AB)=P(A) \cdot P(B)\)
- 条件概率:设 \(A\) 的发生概率为 \(P(A)\),在 \(A\) 发生的情况下 \(B\) 发生的概率为 \(P(B|A)\),则 \(A,B\) 都发生的概率 \(P(AB)=P(A) \cdot P(B|A)\)
\(n\) 个人排队抽 \(n\) 个签,每人抽一个,签里只有一个签是中奖的,无论站在哪里,抽中的概率都是 \(1/n\),可以用条件概率连乘验证。
习题:CF148D Bag of mice
解题思路
状态:设 \(dp_{i,j}\) 表示当前袋中有 \(i\) 只白鼠 \(j\) 只黑鼠时,A 获胜的概率
起点:\(dp_{0,i}=0, dp_{i,0}=1\)
终点:\(dp_{w,b}\)
转移:
- 先手拿到白鼠:
dp[i][j]+=i/(i+j)
- 先手黑鼠,后手白鼠:
dp[i][j]+=0
,这种情况不用处理 - 先手黑鼠,后手黑鼠,跑白鼠:
dp[i][j]+=j/(i+j)*(j-1)/(i+j-1)*i/(i+j-2)*dp[i-1][j-2]
- 先手黑鼠,后手黑鼠,跑黑鼠:
dp[i][j]+=j/(i+j)*(j-1)/(i+j-1)*(j-2)/(i+j-2)*dp[i][j-3]
参考代码
#include <cstdio>
const int N = 1005;
double dp[N][N];
int main()
{
int w, b; scanf("%d%d", &w, &b);
for (int i = 1; i <= w; i++) { // 白鼠
dp[i][0] = 1;
for (int j = 1; j <= b; j++) { // 黑鼠
dp[i][j] += 1.0 * i / (i + j);
double tmp = 1.0 * j / (i + j) * (j - 1) / (i + j - 1);
if (j >= 2) dp[i][j] += tmp * i / (i + j - 2) * dp[i - 1][j - 2];
if (j >= 3) dp[i][j] += tmp * (j - 2) / (i + j - 2) * dp[i][j - 3];
}
}
printf("%.9f\n", dp[w][b]);
return 0;
}
习题:CF768D Jon and Orbs
解题思路
使用动态规划来解决这个问题。设 \(dp_{i,j}\) 表示前 \(i\) 天取到 \(j\) 种物品的概率,考虑第 \(i\) 天的选择,如果第 \(i\) 天抽取到的是旧的种类的物品,这个概率是 \(\frac{j}{k}\),如果抽到的是新的种类的物品,这个概率是 \(\frac{k-j+1}{k}\),因为之前已经选了 \(i-1\) 种物品,第 \(i\) 天刚好选到剩下的 \(k-j+1\) 种物品中的一种。
状态转移方程为:\(dp_{i,j}=dp_{i-1,j-1} \times \frac{k-j+1}{k} + dp_{i-1,j} \times \frac{j}{k}\)。
那么,怎么知道要算到多少天呢?理论上查询结果最大的情况应该是多少天 \(1000\) 种物品收集齐的概率能达到 \(\frac{1}{2}\)。这里可以通过打表的方式将天数不断增大,最终发现大约是 \(7500\) 天不到一点,则最后提交的代码中让 dp 天数枚举到约 \(7500\) 即可。
参考代码
#include <cstdio>
#include <cmath>
const int N = 1005;
const double EPS = 1e-7;
double dp[N * 10][N];
int main()
{
int k, q;
scanf("%d%d", &k, &q);
dp[0][0] = 1.0;
for (int i = 1; i <= 7500; i++)
for (int j = 1; j <= k; j++)
dp[i][j] = dp[i - 1][j - 1] * (k - j + 1) / k + dp[i - 1][j] * j / k;
while (q--) {
int p;
scanf("%d", &p);
for (int i = k; i <= 7500; i++)
if (dp[i][k] >= (p - EPS) / 2000) {
printf("%d\n", i); break;
}
}
return 0;
}
期望
期望用 \(E(x)\) 表示,定义为每种情况的结果乘这种情况发生的概率,再求和。
如果每种情况发生的概率相等,则也可以用结果之和除以情况数。如投掷一个 \(6\) 面的质量分布均匀的骰子,朝上的数的期望是:\(E(x)=1 \times \frac{1}{6} +2\times \frac{1}{6}+3 \times \frac{1}{6}+ 4 \times \frac{1}{6}+ 5 \times \frac{1}{6}+ 6 \times \frac{1}{6}=\frac{1+2+3+4+5+6}{6}=\frac{7}{2}\)
期望具有线性性:
- 对同一问题重复 \(k\) 次,得到的结果的和的期望 \(E(kx)=kE(x)\)(可以理解为扔 \(k\) 次骰子)
- 两个独立问题,得到的结果的和的期望 \(E(x+y)=E(x)+E(y)\)(可以理解为扔两个不同的骰子)
- 结合起来有 \(E(ax+by)=aE(x)+bE(y)\)
注意期望只有线性性质,比如 \(E(x^2)=E^2(x)\) 是不成立的。
如果把发生事件看成 \(1\),不发生事件看成 \(0\),求出的期望就是该事件发生的概率。
首次期望:重复尝试同一件事,单次成功概率为 \(p\),收获第一次成功的期望次数是 \(1/p\)。
习题:P6154 游走
解题思路
本题选择所有路径的概率相等,而路径数和所有路径的总长都不难求,所以可以直接用所有路径的总长除以路径数。这类问题也可以看成是假期望题,真正的考察点是计数。
设 \(cnt_i\) 表示以 \(i\) 为终点的路径数,\(len_i\) 表示以 \(i\) 为终点的路径的总长。对于有向边 u->v
,有状态转移:\(cnt_v = (\sum cnt_u) + 1\)(加 \(1\) 是指 \(v\) 自己原地的一条路径),\(len_v = \sum (len_u + cnt_u)\)(每条路径延伸过来时长度都加 \(1\),有 \(cnt_u\) 条路径延伸过来所以一共加 \(cnt_u\))。
最后答案就是 \(\frac{\sum len_i}{\sum cnt_i}\),取模就是把除法写成乘逆元。
参考代码
#include <cstdio>
#include <vector>
#include <queue>
using std::vector;
using std::queue;
const int N = 1e5 + 5;
const int MOD = 998244353;
vector<int> graph[N];
int ind[N], len[N], cnt[N];
int quickpow(int x, int y) {
int res = 1;
while (y > 0) {
if (y & 1) res = 1ll * res * x % MOD;
x = 1ll * x * x % MOD;
y >>= 1;
}
return res;
}
int main()
{
int n, m; scanf("%d%d", &n, &m);
while (m--) {
int x, y; scanf("%d%d", &x, &y);
graph[x].push_back(y); ind[y]++;
}
queue<int> q;
for (int i = 1; i <= n; i++) {
cnt[i] = 1;
if (ind[i] == 0) q.push(i);
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : graph[u]) {
// u->v
len[v] = (len[v] + (len[u] + cnt[u]) % MOD) % MOD;
cnt[v] = (cnt[v] + cnt[u]) % MOD;
if (--ind[v] == 0) q.push(v);
}
}
int suml = 0, sumc = 0;
for (int i = 1; i <= n; i++) {
suml = (suml + len[i]) % MOD;
sumc = (sumc + cnt[i]) % MOD;
}
printf("%d\n", 1ll * suml * quickpow(sumc, MOD - 2) % MOD);
return 0;
}
习题:P1297 [国家集训队] 单选错位
解题思路
做对 \(i\) 题的情况数或概率很难直接算,但可以分析每道题是否做对,这个事情之间是互相独立的。
根据期望的线性性(多个独立问题的期望可加),可以去看每道题有多大概率做对,也就是有多大概率给答案所求的期望加 \(1\),有多大概率贡献 \(0\),再加起来就是所求的期望了。
第 \(i\) 道题做对的概率是 \(\frac{\min (a_{i-1},a_i)}{a_{i-1}a_i}\),因为第 \(i-1\) 题和第 \(i\) 题的答案总共有 \(a_{i-1}a_i\) 种可能,其中有 \(\min (a_{i-1},a_i)\) 种是两题答案相同的情况,当相邻两题答案一样时错位也能做对。
因此每一题得分的期望是 \(\frac{1}{\max (a_{i-1},a_i)}\),累加起来即可,时间复杂度为 \(O(n)\)。
参考代码
#include <cstdio>
#include <algorithm>
using std::max;
const int N = 1e7 + 5;
int a[N];
int main()
{
int n, A, B, C;
scanf("%d%d%d%d%d", &n, &A, &B, &C, a + 1);
for (int i = 2; i <= n; i++)
a[i] = ((long long) a[i - 1] * A + B) % 100000001;
for (int i = 1; i <= n; i++)
a[i] = a[i] % C + 1;
double ans = 0;
a[n + 1] = a[1];
for (int i = 2; i <= n + 1; i++) ans += 1.0 / max(a[i - 1], a[i]);
printf("%.3f\n", ans);
return 0;
}
习题:P1654 OSU!
解题思路
设 \(dp_i\) 表示前 \(i\) 个操作的期望得分,则有 \(dp_i = dp_{i-1} \cdot (1-p_i) + (dp_{i-1} + E(本次成功时增加的分数)) \cdot p_i = dp_{i-1} + E(本次增加的分数) \cdot p_i\)。
设截止到 \(i-1\),最后一段有连续 \(X\) 个 \(1\),因为有 \((X+1)^3=X^3+3X^2+3X+1\),所以 \(E(本次成功时增加的分数) = 3E(X^2) + 3E(X) + 1\)。
设 \(E1_i\) 表示截止到 \(i\) 时,\(X\) 的期望,\(E2_i\) 表示截止到 \(i\) 时,\(X^2\) 的期望,则有 \(E1_i = (E1_{i-1} + 1) \cdot p_i + 0 \cdot (1-p_i) = (E1_{i-1}+1) \cdot p_i\) 和 \(E2_i = (E2_{i-1} + 2 E1_{i-1} + 1) \cdot p_i + 0 \cdot (1-p_i) = (E2_{i-1}+2E1_{i-1}+1) \cdot p_i\)。
最终 \(dp_i = dp_{i-1} + (3E2_{i-1}+3E1_{i-1}+1) \cdot p_i\)。
参考代码
#include <cstdio>
int main()
{
int n; scanf("%d", &n);
double e1 = 0, e2 = 0, ans = 0;
while (n--) {
double p; scanf("%lf", &p);
ans += p * (3 * e2 + 3 * e1 + 1);
e2 = (e2 + 2 * e1 + 1) * p;
e1 = (e1 + 1) * p;
}
printf("%.1f\n", ans);
return 0;
}
习题:P1850 [NOIP2016 提高组] 换教室
解题思路
首先从一个教室去另一个教室一定会走最短路,所以可以先用 Floyd 算法求一下任意两点的最短路。
考虑 dp,设 \(dp_{i,j,0/1}\) 表示前 \(i\) 个时间段,提了 \(j\) 次申请,本次没提/提了申请的最优解。
枚举上个时间段提了还是没提,列出两个式子,把两个式子取 \(\min\),这个提不提是可以决策的,因此直接决策最优。
列式时如果上次或本次提了申请,算本次增加的距离的时候就要把成功和不成功两种情况的距离分别乘上其概率加起来。
像本题这样每一个事件发生的概率是独立的,就可以直接正推 DP。
时间复杂度为 \(O(v^3+nm)\)。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 2005;
const int V = 305;
const int INF = 1e9;
int c[N], d[N], dist[V][V];
double s[N], f[N], dp[N][N][2];
int query(int i, int x, int y) { // query(i,0/1,0/1)
int pre = (x == 0 ? c[i - 1] : d[i - 1]);
int cur = (y == 0 ? c[i] : d[i]);
return dist[pre][cur];
}
int main()
{
int n, m, v, e;
scanf("%d%d%d%d", &n, &m, &v, &e);
if (m > n) m = n;
for (int i = 1; i <= n; i++) scanf("%d", &c[i]);
for (int i = 1; i <= n; i++) scanf("%d", &d[i]);
for (int i = 1; i <= n; i++) {
scanf("%lf", &s[i]); f[i] = 1 - s[i];
}
for (int i = 1; i <= v; i++) {
for (int j = 1; j <= v; j++)
dist[i][j] = INF;
dist[i][i] = 0;
}
while (e--) {
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
dist[a][b] = dist[b][a] = min(dist[a][b], w);
}
for (int k = 1; k <= v; k++)
for (int i = 1; i <= v; i++)
for (int j = 1; j <= v; j++)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
for (int i = 0; i <= n; i++)
for (int j = 0; j <= m; j++)
dp[i][j][0] = dp[i][j][1] = INF;
dp[1][0][0] = dp[1][1][1] = 0;
for (int i = 2; i <= n; i++) {
dp[i][0][0] = dp[i - 1][0][0] + query(i, 0, 0);
int bound = min(m, i);
for (int j = 1; j <= bound; j++) {
// 上次不申请换,这次不申请换
double tmp = dp[i - 1][j][0] + query(i, 0, 0);
dp[i][j][0] = min(dp[i][j][0], tmp);
// 上次申请换,这次不申请换
tmp = dp[i - 1][j][1];
tmp += s[i - 1] * query(i, 1, 0);
tmp += f[i - 1] * query(i, 0, 0);
dp[i][j][0] = min(dp[i][j][0], tmp);
// 上次不申请换,这次申请换
tmp = dp[i - 1][j - 1][0];
tmp += s[i] * query(i, 0, 1);
tmp += f[i] * query(i, 0, 0);
dp[i][j][1] = min(dp[i][j][1], tmp);
// 上次申请换,这次申请换
tmp = dp[i - 1][j - 1][1];
tmp += s[i - 1] * s[i] * query(i, 1, 1);
tmp += s[i - 1] * f[i] * query(i, 1, 0);
tmp += f[i - 1] * s[i] * query(i, 0, 1);
tmp += f[i - 1] * f[i] * query(i, 0, 0);
dp[i][j][1] = min(dp[i][j][1], tmp);
}
}
// double ans = 0;
double ans = INF;
for (int i = 0; i <= m; i++) {
ans = min(ans, min(dp[n][i][0], dp[n][i][1]));
}
printf("%.2f\n", ans);
return 0;
}
例题:P4316 绿豆蛙的归宿
本题如果按原图正着 dp,对边权为 \(w\) 的一条边 u->v
做 dp[v]+=(dp[u]+w(u,v))/degree[u]
,会发现一个点接收到的总概率不是 \(1\),因此这个 dp 算出来的答案不是正确的期望值。
而且把这个式子拆开会发现每条边 u->v
给最终答案贡献的系数(概率)是一条路径的后缀概率乘积,而显然经过这条边的概率应该是从起点开始直到走到这个点时这条路径的概率的乘积,也就是前缀乘积,这个概率其实是一系列条件概率相乘。
因此改变想法,考虑倒推,设 dp[u]
表示从 \(u\) 出发到终点的长度期望,则有 dp[u]+=(dp[v]+w(u,v))/degree[u]
,可以用记忆化搜索或者建反向图做拓扑排序计算,初值是 dp[n]=0
。
这样,dp[u]
接收到的总概率就是 \(1\),拆开式子也会发现每条边贡献的概率也是对的。
这是期望 dp 的常见做法,尤其是事件发生的概率相当于一系列条件概率连乘的时候,此时倒推有显然的正确性,而正推比较难,本题如果正推需要同时 dp 出从起点走到当前点的概率,作为这条边的边权要乘的系数,能硬做但比较麻烦。
参考代码
#include <cstdio>
#include <vector>
#include <queue>
using namespace std;
const int MAXN = 100005;
struct Edge {
int to, cost;
};
vector<Edge> G[MAXN];
int in[MAXN], d[MAXN];
double dp[MAXN];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G[v].push_back({u, w}); in[u]++; d[u]++;
}
queue<int> q;
for (int i = 1; i <= n; i++) {
if (in[i] == 0) q.push(i);
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (Edge& e : G[u]) {
int v = e.to, w = e.cost;
dp[v] += (dp[u] + w) / d[v];
if (--in[v] == 0) q.push(v);
}
}
printf("%.2f\n", dp[1]);
return 0;
}
习题:P4550 收集邮票
解题思路
设 \(dp_i\) 表示如果初始有 \(i\) 种邮票,直到得到所有的邮票所花的钱数的期望值。
考虑从当前状态出发再抽一张邮票是哪种邮票(之前有的还是没有的),然后让后边所有的邮票化的钱加上这张邮票花钱的钱,而这张邮票花多少钱取决于已经买了多少张票。
所以再设 \(num_i\) 表示如果初始有 \(i\) 种邮票,直到得到所有邮票所买的票数的期望值。初始化 \(dp_n = 0, num_n = 0\)。
可以列出状态转移方程:\(dp_i = \frac{i}{n} \times (dp_i + num_i + 1) + (1-\frac{i}{n}) \times (dp_{i+1}+num_{i+1}+1)\)。
这个式子没法直接推,但这是个等式,可以移项,得 \(dp_i = \frac{i}{n-i} \times num_i + dp_{i+1} + num_{i+1} + \frac{n}{n-i}\),最终答案是 \(dp_0\)。
对于 \(num\),则可以列出式子 \(num_i = \frac{i}{n} \times num_i + (1-\frac{i}{n}) \times num_{i+1}+1\),移项得 \(num_i = num_{i+1} + \frac{n}{n-i}\)。
参考代码
#include <cstdio>
const int N = 10005;
double dp[N], num[N];
int main()
{
int n; scanf("%d", &n);
dp[n] = num[n] = 0;
for (int i = n - 1; i >= 0; i--) {
num[i] = num[i + 1] + 1.0 * n / (n - i);
dp[i] = 1.0 * i / (n - i) * num[i] + dp[i + 1] + num[i + 1] + 1.0 * n / (n - i);
}
printf("%.2f\n", dp[0]);
return 0;
}