概率和期望总结

数学是毒瘤


概率与期望总结。

看这玩意就跟看扩展欧几里得、看矩阵乘法、看组合数学差不多,甚至比那些还难一个档次,因为它还跟 DP 搞在一起,美其名曰:概率 DP期望 DP


概率

定义

  • 某个随机试验的某种可能结果称为 样本点
  • 所有样本点构成的集合称为 样本空间

到这里很好理解,例:掷一个骰子的样本空间为 {1,2,3,4,5,6}

  • 一个样本空间的子集称为 事件

例:在掷一个骰子的样本空间中,“点数小于 7”是一个 必然事件,“点数大于 6”是一个 不可能事件,“点数是 2”是一个 随机事件

  • 若随机事件 A 发生的频率随着试验次数的增加稳定于某个常数上,则把这个常数称为事件 A概率,记为 P(A)

例:在掷一个骰子的样本空间中,“点数不小于 3”的概率为 0.5

事件的关系及运算

  1. 包含关系BA
  2. 相等关系B=A
  3. 并事件(和事件)ABA+B
  4. 交事件(积事件)ABA×B
  5. 互斥事件AB 不可能同时发生,AB=
  6. 对立事件AB 有且仅有一个发生,AB=P(AB)=1

条件概率

定义

在事件 A 发生的条件下,事件 B 发生的条件概率,记作 P(BA)

公式

  • 乘法公式

P(BA)=P(AB)P(A)

  • 全概率公式:设 A1,A2,,An 两两互斥,A1A2An=Ωi[1,n],P(Ai)=1,则:

BΩ,P(B)=i=1nP(Ai)×P(BAi)

期望

定义

若一个事件 A 的样本空间为 {x1,x2,},每个样本点的概率为 pi。设其结果的大小为 X,则 X 的期望值表示事件 A 结果的平均大小,记为 E(X)=pixi。期望是样本值与概率的乘积之和

例如,掷一个骰子的点数为 X

E(X)=(1+2+3+4+5+6)×16=3.5

性质

期望是线性函数。

  1. 满足 E(aX+bY)=a×E(X)+b×E(Y)
  2. 当两个随机变量 XY 相互独立且各自都有一个已定义的数学期望时,满足 E(XY)=E(X)×E(Y)

例如,掷一个骰子的点数 X 的数学期望为 3.5,掷两个骰子的点数 2X 的数学期望为 7

概率 DP 和期望 DP

概率 DP 正推,期望 DP 逆推。

简介

概率 DP

概率 DP 较为简单,当前状态只需由前一状态乘当前概率得出,即 dp[i]=dp[i1]×pi

期望 DP

当求解达到某一目标的期望花费时,由于最终的花费无人知晓(无法从无穷提起),因此期望 DP 需要逆推

dp[i]i 状态到目标状态的期望值(即与目标的差距)。初始时,令 dp[n]=0,然后状态转移,新状态为前一状态与转移概率的乘积再加上转移的花费,即 dp[i1]=dp[i]×pi+wi

所求即为 dp[0]

期望 DP 状态一般都设为已经……还需要……的期望

其实期望 DP 有时也不需要逆推,顺推逆推本质上是没什么区别的。但 DP 重点在于理解方程,挑自己喜欢的方式去推、去理解,能推出来就是好的。

——2024/4/5 upd.

例 0

题意

给定一个 n 面的骰子,求投出 n 个不同的面的期望投掷次数。1n1000

解析

dp[i] 表示已经投出 i 个面,要投出剩余 (ni) 个面的期望投掷次数,显然 dp[n]=0

对于当前状态 i,投一次骰子,有 in 的可能性投中已经出现的 i 个面之一,此时还需投 dp[i] 次;剩余 nin 的概率投出剩余的 (ni) 个面之一,此时还需投 dp[i+1] 次。状态转移方程如下:

dp[i]=dp[i]×in+dp[i+1]×nin+1dp[i]×nin=dp[i+1]×nin+1dp[i]=dp[i+1]+nni

例 1 绿豆蛙的归宿

题意

给出张 n 个点 m 条边的有向无环图,每条边都有一个长度,并且从起点出发能够到达所有的点,所有的点也都能够到达终点。

绿豆蛙从起点出发,走向终点。 到达每一个顶点时,如果该节点有 k 条出边,绿豆蛙可以选择任意一条边离开该点,并且走向每条边的概率为 1/k 。现在绿豆蛙想知道,从起点走到终点的所经过的路径总长度期望是多少?

1n1051m2n。四舍五入保留两位输出。

解析

dp[x] 表示从结点 x 到终点的路径的期望长度。因为最后一个点没有出度,即 dp[n]=0,所以显然逆推。

若从 x 出发有 k 条边,分别到达 y1,y2,,yk,边长分别为 z1,z2,,zk,根据数学期望的定义和性质,有:

dp[x]=1ki=1k(dp[yi]+zi)

目标即为 dp[1]。故我们建反图,执行拓扑排序,在排序的同时计算 dp[x]。时间复杂度 O(n+m)

#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 题目,堪称期望入门三部曲。此为第一道。

题意

给定一个由 o,x,? 组成的长度为 n 的字符串,o 表示成功,x 表示失败,? 表示各有 0.5 的可能性。连续 Xo 就有 X2 的分数,求总期望得分。

解析

先假设连续 xo 就有 x 的分数(一次方的情况),则显然,dp[i]=(dp[i1]+1)×pipi 为概率,本题中 o1x0?0.5)。

到了二次方的情况,设原来的一次方情况为 x1i,方程变为:

dp[i]=[dp[i1]+(x1i1+1)2x1i12]×pi

化简即为:

dp[i]=(dp[i1]+2×x1i1+1)×pi

总之,我们用一次方推二次方,记住这一点。

实现

很简单,不放了。

例 2.2 Let's Play Osu!

与例 2.1 大致相同(双倍经验),就是概率 pi 由指定的 100.5 变为输入的数值。方法同上。

例 2.3 OSU!

例 2.1 和例 2.2 的综合 + 升级版。

题意

给定 n 个点的成功概率 pi,连续 X 个成功就有 X3 的分数,求总期望得分。

解析

从二次方期望到了三次方期望。完全立方公式较为复杂,我们先运用我们小学的数学知识将其展开,得到:

(x+1)3=x3+3x2+3x+1

有了例 2.1 的经验,我们就可以得出三次方的方程:

dp[i]=(dp[i1]+3×x2i1+3×x1i1+1)×pi

你以为这样就可以 AC 了?胡诌!听取 WA 声一片!

为什么呢?因为我们要求的是总期望,也就是 dp[1]dp[n] 的总期望,只输出 dp[n] 当然不对了。

难道是输出 i=1ndp[i] 吗?也不对。格外注意,答案不等于 E(x3)!因为最终答案要求总得分期望(不是总期望得分),所以还需要考虑之前的累计得分。所以最终的状态转移方程为:

dp[i]=dp[i1]+(3×x2i1+3×x1i1+1)×pi

例 3 [NOI2005] 聪聪和可可

题意

聪聪成天想着要吃掉可可。

在一个 N 个点的无向图上(保证无重边),初始时,可可在 M 处,聪聪在 C(C,M[1,N])。以后的每个时间单位,可可会等概率地选择不动或移动一个结点,聪聪每次则会挑一个更靠近可可的结点一步移动至多两个结点(如果有多个则选编号最小的结点)。

每个时间单位,聪聪先走,可可后走。在某一时刻,若聪聪和可可位于同一个景点,则可怜的可可就被吃掉了。求聪聪吃掉可可步数的期望值。1N,E1000

解析

首先,我们观察到猫的走位比较神奇,是定向的,不方便我们进行 DP。所以我们需要先预处理出全源最短路,再据此预处理出当聪聪在每个结点时下一步会走的位置。观察到数据范围不方便 Floyd,某已死算法时间复杂度不保险,于是跑 N 次 Dijkstra 即可求出全源最短路,之后预处理容易。其次由于要统计鼠移动的概率,还需统计每个点的出度。

然后 DP。给定的是一个图,考虑记忆化搜索。DFS 函数给定两个参数分别表示当前猫和鼠的位置,返回当前情况下猫吃到鼠的步数。若猫鼠重合,返回 0;若鼠在猫两个结点之内,返回 1。否则,对鼠的移动分内讨恁。初始化 dp[s,t]=1。若鼠不动,猫向鼠的方向移动两步,易得方程:

dp[s,t]=dfs(nxtnxts,t,t,t)outt+1

若鼠动,枚举相邻结点,猫还是向鼠的方向移动两步,易得方程:

dp[s,t]=vdfs(nxtnxts,t,t,v)outt+1

代码就没什么难度了。

坑点

  1. 初始化 dp 数组为 1!不然无法区分 0 步的情况!
  2. 若不符合特判条件,初始化 dp[s,t]=1 而不是 0

实现

#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 提高组] 换教室

题意

2n 节课程安排在 n 个时间段上。在第 i[1,n] 个时间段上,两节内容相同的课程同时在不同的地点进行,其中,牛牛预先被安排在教室 ci 上课,而另一节课程在教室 di 进行。

如果学生想更换第 i 节课程的教室,则需要提出申请。若申请通过,学生就可以在第 i 个时间段去教室 di 上课,否则仍然在教室 ci 上课。申请更换第 i 节课程的教室被通过的概率是一个已知的实数 ki,并且对于不同课程的申请,被通过的概率是互相独立的。

学校规定,所有的申请只能在学期开始前一次性提交,并且每个人只能选择至多 m 节课程进行申请。可以不用完这 m 个申请的机会,甚至可以一门课程都不申请。

因为不同的课程可能会被安排在不同的教室进行,所以牛牛需要利用课间时间从一间教室赶到另一间教室。

牛牛所在大学的 v 个教室和 e 条道路构成了一个无向图,边有边权。当第 i[1,n) 节课结束时,牛牛就会从这节课的教室出发,选一条边权和最小的路径前往下一节课的教室。

现在牛牛想知道,申请哪几门课程可以使他因在教室间移动耗费的体力值的总和的期望值最小,请你帮他求出这个最小值。

  1. 存在重边和自环。
  2. 请注意区分 n,m,v,e 的意义,n 不是教室的数量,m 不是道路的数量。

对于 100% 的数据,n,m2000v300

解析

对于这种题面贼真长的题目,我们要做的第一件事就是理清头绪。

对于这种给定图的题,我们可能会想到邻接表存图然后 DFS,这是惯用的手法。但这道题例外。因为对于这道题,教室胡球换,导致我们无法找到一个 DFS 的入口。这题本质上是一个最优化问题,难以 DFS 的方式解决,继而我对邻接表存图提出质疑。

说明这题不需要存图,完全依靠 DP 解决。

进而思考不依靠 DFS 的 DP 方程。状态如何?转移如何?边界如何?

对于状态,初步想到设 dp[i,j] 表示上 i 节课换了 j 次教室的答案。但是这样设再往下想就难想出来了,因为你当前状态是和上节课的教室有关,这样设状态显然考虑不到上节课的教室情况。于是方程加一维变为,dp[i,j,0/1] 表示上 i 节课换 j 次教室,当前是否申请换教室。

接下来考虑转移。转移是需要用到教室之间的最短路的,这是一个全源最短路。本题数据,教室不超过 300 间,直接跑一个 Floyd 即可,设求出来的 i,j 之间的最短路表示为 vis[i,j]。然后就可以设转移方程了:

(此题转移方程非常长,最重要的就是理清头绪)

若当前不换教室,情况有两种:上节课是否申请换教室。

  1. 上节课不申请换教室。那么情况就是确定的,路径非常显然:vis[ci1,ci],记为 T1
  2. 上节课申请换教室。一旦申请,就有成功与否两种可能。若成功,概率为 ki1,情况为 vis[di1,ci]×ki1,记为 T2.1;若不成功,概率为 1ki1,情况为 vis[ci1,ci]×(1ki1),记为 T2.2

则当前不换教室的转移方程为:

dp[i,j,0]=min2in,1jmin(i,m){min(dp[i1,j,0]+T1,dp[i1,j,1]+T2.1+T2.2)}

接下来讨论当前申请换教室的情况,那么情况就有四种。路径的推理还是同理的,为了节省篇幅,这里就只放结果了:

T1.1=vis[ci1,ci]×(1ki)T1.2=vis[ci1,di]×kiT2.1=vis[di1,di]×ki×ki1T2.2=vis[di1,ci]×(1ki)×ki1T2.3=vis[ci1,di]×ki×(1ki1)T2.4=vis[ci1,ci]×(1ki)×(1ki1)

dp[i,j,1]=min2in,1jmin(i,m){min(dp[i1,j1,0]+T1.1+T1.2,dp[i1,j1,1]+T2.1+T2.2+T2.3+T2.4)}

一顿操作猛如虎,还差最后一步——设置边界。首先初始化 dp 数组为 (这一步 memset 容易炸,建议暴力枚举初始化)。然后设置 dp[1,0,0]=dp[1,1,1]=0,原因显然。然后对于 i(1,n],设置 dp[i,0,0]=dp[i1,0,0]+vis[ci1,ci],原因也是显然的,当上了 i 节课仍未换过教室且这节课也不换,则不满足 dp[i,j,1] 的转移条件,路径一步求出即可。

答案即为 min0jm{min(dp[n,j,0],dp[n,j,1])}

实现

#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] 游走

题意

给定一个 n 个点 m 条边的无向连通图,初始时小 Z 在 1 号顶点,每一步小 Z 以相等的概率随机选择当前顶点的某条边,沿着这条边走到下一个顶点,获得等于这条边的边权的分数。当小 Z 到达 n 号顶点时游走结束,总分为所有获得的分数之和。

现在,请你对这 m 条边进行编号,使得小 Z 获得的总分的期望值最小。

对于 100% 的数据,保证 2n5001m125000,无重边和自环,且从 1 出发可以到达所有的节点。

解析

首先,要求总分期望值最小。根据贪心的思想,我们只需要对期望经过次数越小的边赋越大的边权即可。

于是问题转化为求每条边的期望经过次数

但是边的期望经过次数我们并不好求,因为边的信息很少,没有办法再进行转移。我们注意到题目保证了无重边自环,这就让我们能够通过两个端点来判边,而点的信息包含了与之相连的其他结点,如此一来便能够转移了。

dp[i] 表示第 i 个点的期望经过次数。

暂且不考虑转移方程,我们先要知道如何通过点的期望经过次数推导到边的期望经过次数。令 f[i] 表示第 i 条边的期望经过次数,outi 表示第 i 个点的出度。从边的两个端点出发,要经过这条边各自有 1outi 种可能。令 Ei=(u,v),则有:

f[i]=dp[u]outu+dp[v]outv

这一点明确了,我们就可以安心地设转移方程了。我们知道第 i 个点可以由与 i 相连的 outi 个点转移过来,于是有:

dp[i]=vndp[v]outv

但是存在特殊情况。首先我们初始就在 1 号点,所以 i=1 时的期望要 +1;其次由于到 n 号点就停止游走了,别的点不能由 n 号点转移过来,n 号点自然也无法从别的点转移过来,所以不能考虑 n 号点的贡献。

于是最终的转移方程形成了一个方程组:

dp[i]={dp[1]=vndp[v]outv+1dp[2]=vndp[v]outvdp[n1]=vndp[v]outv

这是有后效性的,需要高斯消元。这就是高斯消元在期望 DP 上的应用。一般来说,树可以直接 DP,有后效性的图可能需要高斯消元。

时间复杂度 O(n3)

实现

#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;
}
posted @   Laoshan_PLUS  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示