概率和期望总结
数学是毒瘤
概率与期望总结。
看这玩意就跟看扩展欧几里得、看矩阵乘法、看组合数学差不多,甚至比那些还难一个档次,因为它还跟 DP 搞在一起,美其名曰:概率 DP 和 期望 DP。
概率
定义
- 某个随机试验的某种可能结果称为 样本点
- 所有样本点构成的集合称为 样本空间
到这里很好理解,例:掷一个骰子的样本空间为
- 一个样本空间的子集称为 事件
例:在掷一个骰子的样本空间中,“点数小于
- 若随机事件
发生的频率随着试验次数的增加稳定于某个常数上,则把这个常数称为事件 的 概率,记为
例:在掷一个骰子的样本空间中,“点数不小于
事件的关系及运算
- 包含关系:
- 相等关系:
- 并事件(和事件):
或 - 交事件(积事件):
或 - 互斥事件:
和 不可能同时发生, - 对立事件:
和 有且仅有一个发生, 且
条件概率
定义
在事件
公式
- 乘法公式
- 全概率公式:设
两两互斥, 且 ,则:
期望
定义
若一个事件
例如,掷一个骰子的点数为
性质
期望是线性函数。
- 满足
。 - 当两个随机变量
和 相互独立且各自都有一个已定义的数学期望时,满足 。
例如,掷一个骰子的点数
概率 DP 和期望 DP
概率 DP 正推,期望 DP 逆推。
简介
概率 DP
概率 DP 较为简单,当前状态只需由前一状态乘当前概率得出,即
期望 DP
当求解达到某一目标的期望花费时,由于最终的花费无人知晓(无法从无穷提起),因此期望 DP 需要逆推。
设
所求即为
期望 DP 状态一般都设为已经……还需要……的期望。
其实期望 DP 有时也不需要逆推,顺推逆推本质上是没什么区别的。但 DP 重点在于理解方程,挑自己喜欢的方式去推、去理解,能推出来就是好的。
——2024/4/5 upd.
例 0
题意
给定一个
解析
设
对于当前状态
例 1 绿豆蛙的归宿
题意
给出张
绿豆蛙从起点出发,走向终点。 到达每一个顶点时,如果该节点有
解析
设
若从
目标即为
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 1e5 + 5;
int n, m, out[MAXN];
double dp[MAXN];
bool vis[MAXN];
vector<pair<int, int>> g[MAXN];
queue<int> q;
void dfs(int u) {
if (u == n) {
dp[u] = 0;
return;
}
vis[u] = 1;
for (auto vv : g[u]) {
int v = vv.first, w = vv.second;
if (!vis[v]) {
dfs(v);
dp[u] += (double)(dp[v] + w) / out[u];
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1, u, v, w; i <= m; i++) {
cin >> u >> v >> w;
g[u].push_back(make_pair(v, w));
out[u]++;
}
dfs(1);
cout << fixed << setprecision(2) << dp[1] << '\n';
return 0;
}
例 2.1 WJMZBMR打osu! / Easy
洛谷上有三道 OSU 题目,堪称期望入门三部曲。此为第一道。
题意
给定一个由
解析
先假设连续
到了二次方的情况,设原来的一次方情况为
化简即为:
总之,我们用一次方推二次方,记住这一点。
实现
很简单,不放了。
例 2.2 Let's Play Osu!
与例 2.1 大致相同(双倍经验),就是概率
例 2.3 OSU!
例 2.1 和例 2.2 的综合 + 升级版。
题意
给定
解析
从二次方期望到了三次方期望。完全立方公式较为复杂,我们先运用我们小学的数学知识将其展开,得到:
有了例 2.1 的经验,我们就可以得出三次方的方程:
你以为这样就可以 AC 了?胡诌!听取 WA 声一片!
为什么呢?因为我们要求的是总期望,也就是
难道是输出
例 3 [NOI2005] 聪聪和可可
题意
聪聪成天想着要吃掉可可。
在一个
每个时间单位,聪聪先走,可可后走。在某一时刻,若聪聪和可可位于同一个景点,则可怜的可可就被吃掉了。求聪聪吃掉可可步数的期望值。
解析
首先,我们观察到猫的走位比较神奇,是定向的,不方便我们进行 DP。所以我们需要先预处理出全源最短路,再据此预处理出当聪聪在每个结点时下一步会走的位置。观察到数据范围不方便 Floyd,某已死算法时间复杂度不保险,于是跑
然后 DP。给定的是一个图,考虑记忆化搜索。DFS 函数给定两个参数分别表示当前猫和鼠的位置,返回当前情况下猫吃到鼠的步数。若猫鼠重合,返回
若鼠动,枚举相邻结点,猫还是向鼠的方向移动两步,易得方程:
代码就没什么难度了。
坑点
- 初始化
数组为 !不然无法区分 步的情况! - 若不符合特判条件,初始化
而不是 !
实现
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 1005;
int n, e, c, m;
vector<int> g[MAXN];
int out[MAXN];
priority_queue<pair<int, int>> q;
int dis[MAXN][MAXN], nxt[MAXN][MAXN];
bool vis[MAXN];
double dp[MAXN][MAXN];
void dijkstra(int s) {
memset(vis, 0, sizeof(vis));
dis[s][s] = 0;
q.push(make_pair(0, s));
while (!q.empty()) {
int u = q.top().second;
q.pop();
if (vis[u]) continue;
vis[u] = 1;
for (auto v : g[u]) {
if (dis[s][v] > dis[s][u] + 1) {
dis[s][v] = dis[s][u] + 1;
q.push(make_pair(-dis[s][v], v));
}
}
}
}
double dfs(int s, int t) {
if (dp[s][t] != -1) return dp[s][t];
if (s == t) return 0;
if (nxt[s][t] == t || nxt[nxt[s][t]][t] == t) return 1;
dp[s][t] = 1;
dp[s][t] += dfs(nxt[nxt[s][t]][t], t) / (out[t] + 1);
for (auto v : g[t]) dp[s][t] += dfs(nxt[nxt[s][t]][t], v) / (out[t] + 1);
return dp[s][t];
}
int main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
cin >> n >> e >> c >> m;
for (int i = 1, u, v; i <= e; i++) {
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
out[u]++, out[v]++;
}
memset(dis, 0x3f, sizeof(dis));
memset(nxt, 0x3f, sizeof(nxt));
for (int i = 1; i <= n; i++) dijkstra(i);
for (int i = 1; i <= n; i++)
for (auto v : g[i])
for (int j = 1; j <= n; j++)
if (dis[i][j] > dis[v][j])
nxt[i][j] = min(nxt[i][j], v);
for (auto &x : dp) for (auto &y : x) y = -1;
cout << fixed << setprecision(3) << dfs(c, m) << '\n';
return 0;
}
从此,难度开始上涨。
例 4 [NOIP2016 提高组] 换教室
题意
有
如果学生想更换第
学校规定,所有的申请只能在学期开始前一次性提交,并且每个人只能选择至多
因为不同的课程可能会被安排在不同的教室进行,所以牛牛需要利用课间时间从一间教室赶到另一间教室。
牛牛所在大学的
现在牛牛想知道,申请哪几门课程可以使他因在教室间移动耗费的体力值的总和的期望值最小,请你帮他求出这个最小值。
- 存在重边和自环。
- 请注意区分
的意义, 不是教室的数量, 不是道路的数量。
对于
解析
对于这种题面贼真长的题目,我们要做的第一件事就是理清头绪。
对于这种给定图的题,我们可能会想到邻接表存图然后 DFS,这是惯用的手法。但这道题例外。因为对于这道题,教室胡球换,导致我们无法找到一个 DFS 的入口。这题本质上是一个最优化问题,难以 DFS 的方式解决,继而我对邻接表存图提出质疑。
说明这题不需要存图,完全依靠 DP 解决。
进而思考不依靠 DFS 的 DP 方程。状态如何?转移如何?边界如何?
对于状态,初步想到设
接下来考虑转移。转移是需要用到教室之间的最短路的,这是一个全源最短路。本题数据,教室不超过
(此题转移方程非常长,最重要的就是理清头绪)
若当前不换教室,情况有两种:上节课是否申请换教室。
- 上节课不申请换教室。那么情况就是确定的,路径非常显然:
,记为 。 - 上节课申请换教室。一旦申请,就有成功与否两种可能。若成功,概率为
,情况为 ,记为 ;若不成功,概率为 ,情况为 ,记为 。
则当前不换教室的转移方程为:
接下来讨论当前申请换教室的情况,那么情况就有四种。路径的推理还是同理的,为了节省篇幅,这里就只放结果了:
一顿操作猛如虎,还差最后一步——设置边界。首先初始化 memset
容易炸,建议暴力枚举初始化)。然后设置
答案即为
实现
#include <bits/stdc++.h>
#define a(i, j) vis[i][j]
using namespace std;
using pii = pair<int, int>;
constexpr int MAXN = 2005;
int n, m, v, e, c[MAXN], d[MAXN];
vector<pii> g[MAXN];
double k[MAXN], dp[MAXN][MAXN][2], ans = INT_MAX;
int vis[MAXN][MAXN]; // 邻接矩阵存图+最短路
int main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
cin >> n >> m >> v >> e;
for (int i = 1; i <= n; i++) cin >> c[i];
for (int i = 1; i <= n; i++) cin >> d[i];
for (int i = 1; i <= n; i++) cin >> k[i];
memset(vis, 0x3f, sizeof(vis));
for (int i = 1, a, b, w; i <= e; i++) {
cin >> a >> b >> w;
vis[a][b] = vis[b][a] = min(vis[a][b], w);
}
for (int k = 1; k <= v; k++)
for (int i = 1; i <= v; i++)
for (int j = 1; j <= v; j++)
vis[i][j] = min(vis[i][j], vis[i][k] + vis[k][j]);
for (int i = 1; i <= v; i++) vis[i][i] = vis[0][i] = vis[i][0] = 0;
for (auto &x : dp) for (auto &y : x) y[0] = y[1] = INT_MAX;
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] + vis[c[i - 1]][c[i]];
for (int j = 1; j <= min(i, m); j++) {
dp[i][j][0] = min(dp[i][j][0], min(dp[i - 1][j][0] + a(c[i - 1], c[i]), dp[i - 1][j][1] + a(d[i - 1], c[i]) * k[i - 1] + a(c[i - 1], c[i]) * (1 - k[i - 1])));
dp[i][j][1] = min(dp[i][j][1],
min(dp[i - 1][j - 1][0] + a(c[i - 1], c[i]) * (1 - k[i]) + a(c[i - 1], d[i]) * k[i],
dp[i - 1][j - 1][1] + a(d[i - 1], d[i]) * k[i] * k[i - 1] + a(d[i - 1], c[i]) * k[i - 1] * (1 - k[i]) + a(c[i - 1], c[i]) * (1 - k[i - 1]) * (1 - k[i]) + a(c[i - 1], d[i]) * (1 - k[i - 1]) * k[i]));
}
}
for (int j = 0; j <= m; j++) ans = min(ans, min(dp[n][j][0], dp[n][j][1]));
cout << fixed << setprecision(2) << ans << '\n';
return 0;
}
例 5 [HNOI2013] 游走
题意
给定一个
现在,请你对这
对于
解析
首先,要求总分期望值最小。根据贪心的思想,我们只需要对期望经过次数越小的边赋越大的边权即可。
于是问题转化为求每条边的期望经过次数。
但是边的期望经过次数我们并不好求,因为边的信息很少,没有办法再进行转移。我们注意到题目保证了无重边自环,这就让我们能够通过两个端点来判边,而点的信息包含了与之相连的其他结点,如此一来便能够转移了。
设
暂且不考虑转移方程,我们先要知道如何通过点的期望经过次数推导到边的期望经过次数。令
这一点明确了,我们就可以安心地设转移方程了。我们知道第
但是存在特殊情况。首先我们初始就在
于是最终的转移方程形成了一个方程组:
这是有后效性的,需要高斯消元。这就是高斯消元在期望 DP 上的应用。一般来说,树可以直接 DP,有后效性的图可能需要高斯消元。
时间复杂度
实现
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 505, MAXM = 125005;
constexpr double eps = 1e-6;
int n, m, out[MAXN], u[MAXM], v[MAXM];
vector<int> g[MAXN];
double a[MAXN][MAXN], b[MAXM], ans;
bool gauss_jordan() {
for (int i = 1; i <= n; i++) {
int r = i;
for (int k = i; k <= n; k++)
if (fabs(a[k][i]) > eps) {
r = k;
break;
}
if (r != i) swap(a[r], a[i]);
if (fabs(a[i][i]) < eps) return 0;
for (int k = 1; k <= n; k++) {
if (k == i) continue;
double t = a[k][i] / a[i][i];
for (int j = i; j <= n + 1; j++)
a[k][j] -= t * a[i][j];
}
}
for (int i = 1; i <= n; i++) a[i][n + 1] /= a[i][i];
return 1;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
cin >> u[i] >> v[i];
g[u[i]].push_back(v[i]);
g[v[i]].push_back(u[i]);
out[u[i]]++, out[v[i]]++;
}
for (int i = 1; i < n; i++) {
a[i][i] = 1;
a[i][n] = i == 1;
for (auto v : g[i])
if (v != n)
a[i][v] = -1.0 / out[v];
}
n--;
gauss_jordan();
n++;
for (int i = 1; i <= m; i++) b[i] = a[u[i]][n] / out[u[i]] + a[v[i]][n] / out[v[i]];
sort(b + 1, b + m + 1, greater<double>());
for (int i = 1; i <= m; i++) ans += i * b[i];
cout << fixed << setprecision(3) << ans << '\n';
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】