概率期望
概率
概率:随机事件发生的可能性,是一个
古典概型:
古典概型的特点:
- 有限性:所有可能出现的基本事件只有有限个
- 等可能性:每个基本事件出现的可能性相等
如投掷一个
计算概率:分类用加法原理,分步用乘法原理。
概率加法:两个不会同时发生的事件
概率乘法:
- 独立事件:两个互相独立的事件
同时发生的概率 - 条件概率:设
的发生概率为 ,在 发生的情况下 发生的概率为 ,则 都发生的概率
习题:CF148D Bag of mice
解题思路
状态:设
起点:
终点:
转移:
- 先手拿到白鼠:
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
解题思路
使用动态规划来解决这个问题。设
状态转移方程为:
那么,怎么知道要算到多少天呢?理论上查询结果最大的情况应该是多少天
参考代码
#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; }
期望
期望用
如果每种情况发生的概率相等,则也可以用结果之和除以情况数。如投掷一个
期望具有线性性:
- 对同一问题重复
次,得到的结果的和的期望 (可以理解为扔 次骰子) - 两个独立问题,得到的结果的和的期望
(可以理解为扔两个不同的骰子) - 结合起来有
注意期望只有线性性质,比如
如果把发生事件看成
首次期望:重复尝试同一件事,单次成功概率为
习题:P6154 游走
解题思路
本题选择所有路径的概率相等,而路径数和所有路径的总长都不难求,所以可以直接用所有路径的总长除以路径数。这类问题也可以看成是假期望题,真正的考察点是计数。
设 u->v
,有状态转移:
最后答案就是
参考代码
#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 [国家集训队] 单选错位
解题思路
做对
根据期望的线性性(多个独立问题的期望可加),可以去看每道题有多大概率做对,也就是有多大概率给答案所求的期望加
第
因此每一题得分的期望是
参考代码
#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!
解题思路
设
设截止到
设
最终
参考代码
#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。
时间复杂度为
参考代码
#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,对边权为 u->v
做 dp[v]+=(dp[u]+w(u,v))/degree[u]
,会发现一个点接收到的总概率不是
而且把这个式子拆开会发现每条边 u->v
给最终答案贡献的系数(概率)是一条路径的后缀概率乘积,而显然经过这条边的概率应该是从起点开始直到走到这个点时这条路径的概率的乘积,也就是前缀乘积,这个概率其实是一系列条件概率相乘。
因此改变想法,考虑倒推,设 dp[u]
表示从 dp[u]+=(dp[v]+w(u,v))/degree[u]
,可以用记忆化搜索或者建反向图做拓扑排序计算,初值是 dp[n]=0
。
这样,dp[u]
接收到的总概率就是
这是期望 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 收集邮票
解题思路
设
考虑从当前状态出发再抽一张邮票是哪种邮票(之前有的还是没有的),然后让后边所有的邮票化的钱加上这张邮票花钱的钱,而这张邮票花多少钱取决于已经买了多少张票。
所以再设
可以列出状态转移方程:
这个式子没法直接推,但这是个等式,可以移项,得
对于
参考代码
#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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
2023-07-25 贪心基础
2023-07-25 函数与结构体
2023-07-25 2.2 字符串 参考代码