概率期望 dp
概率期望 dp
概念
期望,是每次可能结果的概率乘上结果的权值的总和。
对于一个变量 \(X\),如果它有 \(n\) 种取值,第 \(i\) 种取值 \(x_i\) 的概率是 \(P(x_i)\),那么它的期望就是 \(E(X) = \sum\limits_{i = 1} ^ n x _ i P(x_i)\)。
举个例子:如果你有一个骰子,每次可以扔出 \(1 \sim 6\) 中的任意一个数,并且取得每个数的概率是相同的,那么它的取值的期望就是 \(E(X) = \frac{1}{6} \times 1 + \frac{1}{6} \times 2 + \frac{1}{6} \times 3 + \frac{1}{6} \times 4 + \frac{1}{6} \times 5 + \frac{1}{6} \times 6 = \frac{7}{2}\)。
性质
线性性
若随机变量 \(X, Y\) 的期望存在,则:
- 对于任意实数 \(a, b\),都有 \(E(aX + b) = a \cdot E(X) + b\)。
- \(E(X + Y) = E(X) + E(Y)\)。
随机变量乘积的期望
若随机变量 \(X, Y\) 的期望存在且 \(X, Y\) 相互独立,则:
综上,当随机变量 \(X, Y\) 的期望存在且 \(X, Y\) 相互独立时,对于任意实数 \(a, b\),\(E(aX + bY) = a \cdot E(X) + b \cdot E(Y)\)。
概率期望 dp
通常在求解达到某一目标的期望代价时,我们并不知道最终代价,因此,通常我们采用 倒序 的方式进行 dp。
Gym-105284C
题意
有一个由 \(n\) 个结点组成的链,对于每个结点 \(i\),有 \(\frac{1}{i}\) 的概率保留它,还有 \(1 - \frac{1}{i}\) 的概率删除它,求出这之后的连通块的期望数量,答案对 \(10 ^ 9 + 7\)。
思路
我们先画出一条链:
我们考虑连通块的数量变化。
- 当图中的点的数量 \(+ 1\) 时,连通块的数量也 \(+1\)。
- 当图中的边的数量 \(+ 1\) 时,连通块的数量 \(-1\)。
我们再写出每个点和每条边保留的概率,对于点 \(i\),假设前 \(i - 1\) 个点已经考虑完了,则点 \(i\) 可以形成新连通块的概率就是点 \(i\) 保留的概率减去 \(i - 1\) 到 \(i\) 的边保留的概率,也就是 \(\frac{1}{i} - \frac{1}{i} \cdot \frac{1}{i - 1}\)。
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6 + 10, mod = 1e9 + 7;
int T, n;
ll inv[N], ans[N];
void Solve() {
cin >> n;
cout << ans[n] << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
inv[1] = 1;
for (int i = 2; i < N; i++) {
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
}
for (int i = 1; i < N; i++) {
ans[i] = (ans[i - 1] + inv[i] - inv[i] * inv[i - 1] % mod + mod) % mod;
}
cin >> T;
while (T--) Solve();
return 0;
}
CF1778D
题意
给定两个长度为 \(n\) 的二进制字符串 \(a, b\),每次操作随机选择一个下标 \(i\),将字符 \(a_i\) 取反。
求出第一次使得 \(a, b\) 相等的操作次数。
思路 1
设 \(dp_i\) 表示当前剩下 \(i\) 个不同的位置,那么转移就是:
其中,选中不同的位置的概率是 \(\frac{i}{n}\),操作后就只剩下 \(i - 1\) 个不同的位置了。
而选中原本已经相同的位置的概率是 \(\frac{n - i}{n}\),操作后就有 \(i + 1\) 个不同的位置。
初始状态 \(dp_0 = 0\)。
但是这种转移是不存在拓扑序的,状态 \(i\) 既会从状态 \(i - 1\) 转移过来,也会从状态 \(i + 1\) 转移过来。
我们设 \(dp_1 = x, n = 4\),然后用解方程的方式将每个 \(dp_i\) 表示成 \(kx + b\) 的形式,那么
\(dp_1 = 1 + \frac{1}{4} \cdot dp_0 + \frac{3}{4} \cdot dp_2 = 1 + \frac{1}{4} \cdot 0 + \frac{3}{4} \cdot dp_2\),可以解出 \(dp_2 = \frac{4}{3} \cdot (x - 1) = \frac{4}{3} x - \frac{4}{3}\)
\(dp_2 = 1 + \frac{2}{4} \cdot dp_1 + \frac{2}{4} \cdot dp_3 = 1 + \frac{2}{4} x + \frac{2}{4} \cdot dp_3\),可以解出 \(dp_3 = \frac{5}{3}x - \frac{14}{3}\)
\(dp_3 = 1 + \frac{3}{4} \cdot dp_2 + \frac{1}{4} \cdot dp_4 = 1 + \frac{3}{4} \cdot (\frac{4}{3} x - \frac{4}{3}) + \frac{1}{4} \cdot dp_4\),可以解出 \(dp_4 = \frac{8}{3}x - \frac{56}{3}\)
又有 \(dp_4 = 1 + dp_3 = 1 + \frac{5}{3}x - \frac{14}{3}\),可以解出 \(x = 15\)。
设 \(a\) 与 \(b\) 中不同的字符数量为 \(cnt\),我们就可以将解出来的 \(x\) 代入 \(dp_{cnt}\),算出答案。
代码 1
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<ll, ll>;
const int N = 1e6 + 10, mod = 998244353;
int T, n;
ll inv[N];
string a, b;
pii dp[N];
ll qpow(ll x, ll y) {
if (!y) return 1;
ll tmp = qpow(x, y / 2);
return tmp * tmp % mod * (y & 1 ? x : 1) % mod;
}
void Solve() {
cin >> n >> a >> b;
int cnt = 0;
for (int i = 0; i < n; i++) cnt += a[i] != b[i];
dp[0] = {0, 0}, dp[1] = {1, 0};
for (int i = 2; i <= n; i++) {
ll p = (i - 1) * inv[n] % mod, q = n * inv[n - i + 1] % mod;
dp[i].first = (dp[i - 1].first - p * dp[i - 2].first % mod + mod) % mod * q % mod;
dp[i].second = (dp[i - 1].second - 1 - dp[i - 2].second * p % mod + mod) % mod * q % mod;
}
ll p = (1 + dp[n - 1].second % mod - dp[n].second + mod) % mod;
ll q = (dp[n].first - n * inv[n] % mod * dp[n - 1].first % mod + mod) % mod;
ll k = p * qpow(q, mod - 2) % mod;
cout << (k * dp[cnt].first % mod + dp[cnt].second) % mod << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
inv[1] = 1;
for (int i = 2; i < N; i++) {
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
}
cin >> T;
while (T--) Solve();
return 0;
}
思路 2
设 \(cost_i\) 表示从剩余 \(i\) 个不同位置转移到剩余 \(i - 1\) 个不同位置需要的次数的期望。
这种时候转移有两种情况:
- 修改的位置恰好不同,概率是 \(\frac{i}{n}\)。
- 修改的位置是相同的,那么不同的位置的数量在操作后就会变成 \(i + 1\) 个,概率是 \(\frac{n - i}{n}\),那么之后还需要从 \(i + 1\) 转移到 \(i\),再转移到 \(i - 1\)。
因此,我们的转移是:\(cost_i = \frac{n - i}{n} (1 + cost_{i + 1} + cost_i) + \frac{i}{n}\)。
移项后就会变成:\(cost_i = \frac{(n - i)cost_{i + 1} + n}{i} = (\frac{n}{i} - 1)cost_{i + 1} + \frac{n}{i}\)。
代码 2
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<ll, ll>;
const int N = 1e6 + 10, mod = 998244353;
int T, n;
ll inv[N], dp[N];
string a, b;
void Solve() {
cin >> n >> a >> b;
int cnt = 0;
for (int i = 0; i < n; i++) cnt += a[i] != b[i];
dp[n] = 1;
ll ans = (n == cnt);
for (int i = n - 1; i >= 1; i--) {
dp[i] = (n * inv[i] % mod + (n * inv[i] % mod + mod - 1) % mod * dp[i + 1] % mod) % mod;
if (i <= cnt) (ans += dp[i]) %= mod;
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
inv[1] = 1;
for (int i = 2; i < N; i++) {
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
}
cin >> T;
while (T--) Solve();
return 0;
}
洛谷 P8774
题意
有一只甲壳虫想要爬到高度 \(n\),它最开始位于高度 \(0\),当它尝试从高度 \(i - 1\) 爬到高度 \(i\) 时有 \(p_i\) 的概率会掉回高度 \(0\) 的位置,求爬到高度 \(n\) 所经过的时间的期望值。
思路
我们考虑上个例题的思路 1,还是令 \(dp_i\) 表示从 \(i\) 爬到 \(n\) 的期望时间。
那么转移就是:\(dp_i = p_{i + 1} dp_0 + (1 - p_{i + 1}) dp_{i + 1} + 1\)。
初始状态为 \(dp_n = 0\)。
然后就像上一题一样解方程即可。
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<ll, ll>;
const int N = 1e6 + 10, mod = 998244353;
int n;
ll p[N];
pii dp[N];
ll qpow(ll x, ll y) {
if (!y) return 1;
ll tmp = qpow(x, y / 2);
return tmp * tmp % mod * (y & 1 ? x : 1) % mod;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1, x, y; i <= n; i++) cin >> x >> y, p[i] = x * qpow(y, mod - 2) % mod;
dp[n] = {0, 0}, dp[n - 1] = {p[n], 1};
for (int i = n - 2; i >= 0; i--) {
dp[i].first = (p[i + 1] + dp[i + 1].first - p[i + 1] * dp[i + 1].first % mod + mod) % mod;
dp[i].second = ((mod + 1 - p[i + 1]) % mod * dp[i + 1].second % mod + 1) % mod;
}
ll p = (mod + 1 - dp[0].first) % mod;
ll k = dp[0].second * qpow(p, mod - 2) % mod;
cout << k;
return 0;
}
CF280C
题意
给定一颗有 \(n\) 个结点,并且以 \(1\) 为根的树,你可以进行很多次操作,每次操作会删除一个仍存在于树上的结点的子树。
每次操作都会从剩下的结点中平均的选择一个结点,请求出操作数量的期望。
思路
我们考虑每个结点 \(u\) 对答案的贡献。
对于一个结点 \(u\),如果它会成为被选中的点(不是在某个点的子树中被删除的),那么,它的所有祖先都是在它之前被选中的。
很显然的,结点 \(u\) 的祖先一共有 \(dep_u\) 个,那么,我们考虑 \(u\) 在操作序列中排在所有祖先的前面的概率。
我们又可以发现,这个概率只与 \(u\) 和 \(u\) 的祖先有关,与其他 \(n - dep_u - 1\) 个点并没有关系。
所以,我们考虑在 \(dep_u + 1\) 个点中,点 \(u\) 排在第一个的概率,这个就是 \(\frac{1}{dep_u + 1}\)。
直接算出深度求和即可。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n;
vector<int> g[N];
double ans;
void dfs(int u, int fa, int dep) {
ans += 1.0 / dep;
for (int v : g[u]) {
if (v != fa) dfs(v, u, dep + 1);
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
g[u].push_back(v), g[v].push_back(u);
}
dfs(1, 0, 1);
cout << fixed << setprecision(7) << ans;
return 0;
}
abc333 F
题意
有 \(n\) 个人站成一排,第 \(i\) 个人站在从前往后的第 \(i\) 个位置。
我们需要重复一个操作,直到队伍中只剩下一个人,每次操作有 \(\frac{1}{2}\) 的概率让队伍中的第一个人离开,又有 \(\frac{1}{2}\) 的概率让他走到队伍末尾。
对于每一个 \(i \ (1 \le i \le n)\),求出第 \(i\) 个人最终留在队伍里的期望。
思路
我们设 \(dp_{i, j}\) 表示还剩 \(i\) 个人时,第 \(j\) 个人最终留下的期望。
很显然,初始状态为 \(dp_{1, 1} = 1\)。
对于其他的状态,则有 \(dp_{i, j} = \frac{1}{2}(dp_{i, (j - 1) \bmod n} + dp_{i - 1, j - 1})\)。
其中,我们有 \(\frac{1}{2}\) 的概率将第一个人移到最后,那么第 \(j\) 个人的编号就会变小一个。
同样的,还有另外 \(\frac{1}{2}\) 的概率让第一个人离开,那么第 \(j\) 个人的编号就会变小,同时人数也会变少。
但是,我们发现转移中出现了环,因此,我们考虑像上两题一样解方程的做法。
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<ll, ll>;
const int N = 3010, mod = 998244353;
int n;
pii dp[N][N];
ll qpow(ll x, ll y) {
if (!y) return 1;
ll tmp = qpow(x, y / 2);
return tmp * tmp % mod * (y & 1 ? x : 1) % mod;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
ll inv = (mod + 1) / 2;
dp[1][1] = {0, 1};
for (int i = 2; i <= n; i++) {
dp[i][1] = {1, 0};
for (int j = 2; j <= i; j++) {
dp[i][j].first = dp[i][j - 1].first * inv % mod;
dp[i][j].second = (dp[i - 1][j - 1].second + dp[i][j - 1].second) % mod * inv % mod;
}
ll p = dp[i][i].second, q = (2 - dp[i][i].first + mod) % mod;
ll k = p * qpow(q, mod - 2) % mod;
dp[i][1].second = k;
for (int j = 2; j <= i; j++) {
dp[i][j].second = (dp[i - 1][j - 1].second + dp[i][j - 1].second) % mod * inv % mod;
}
}
for (int i = 1; i <= n; i++) cout << dp[n][i].second << ' ';
return 0;
}
CF1418E
题意
有 \(n\) 只怪物,它们会发出攻击,你有 \(m\) 个盾牌,每个盾牌有两个数值:耐久度 \(a\) 与防御等级 \(b\),每只怪物有一个数值:力量 \(d\)。
当你对抗力量为 \(d\) 的怪物时,有三种情况:
-
如果 \(a = 0\),你会收到 \(d\) 点伤害。
-
如果 \(a > 0, d \ge b\),你不会受到伤害,但是盾牌的耐久度会减少 \(1\)。
-
如果 \(a > 0, d < b\),则什么都不会发生。
你将会按照某种随机的顺序对抗怪物,对于每一个 \(i \ (1 \le i \le m)\),请求出当你持着盾牌 \(i\) 时,你受到的伤害的期望。
思路
首先,我们会发现,对于一个盾牌 \(i\),如果它的耐久度为 \(a\),防御等级为 \(b\),那么实际上所有怪物只分为两种:一种是可以使耐久度减少的,另一种是不能使耐久度减少的。
假设对于盾牌 \(i\),有 \(x\) 只怪物可以使它的耐久度减少,我们考虑对于每只怪物,它的贡献:
首先,如果 \(x < a\),自然是不需要考虑的,耐久度不会掉完。
那么,对于 \(x \ge a\) 的情况,对于第 \(k\) 只怪物,如果想要造成伤害,就需要在 \(k\) 之前有 \(a\) 只可以减少耐久度的怪物发动攻击。
分为两种情况:
- 如果 \(d_k \ge b\),这样的概率是 \(\frac{x - a}{x}\)。
- 如果 \(d_k < b\),概率是 \(\frac{x - a + 1}{x + 1}\)。
可以发现,对于这两种怪物,每种怪物中的每一个被选中的概率都是一样的,所以可以直接合并到一起,用前缀和处理。
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 10, mod = 998244353;
int n, m, d[N];
ll sum[N], inv[N];
ll qpow(ll x, ll y) {
if (!y) return 1;
ll tmp = qpow(x, y / 2);
return tmp * tmp % mod * (y & 1 ? x : 1) % mod;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m, inv[1] = 1;
for (int i = 2; i <= n; i++) inv[i] = (mod - mod / i) * inv[mod % i] % mod;
for (int i = 1; i <= n; i++) cin >> d[i];
sort(d + 1, d + n + 1);
for (int i = 1; i <= n; i++) sum[i] = (sum[i - 1] + d[i]) % mod;
while (m--) {
int a, b; cin >> a >> b;
int pos = lower_bound(d + 1, d + n + 1, b) - d - 1;
int x = n - pos;
ll ans = 0;
if (x >= a) {
ans = ((x - a) * inv[x] % mod * (sum[n] - sum[pos] + mod) % mod + (x - a + 1) * inv[x + 1] % mod * sum[pos] % mod) % mod;
}
cout << ans << '\n';
}
return 0;
}
CF1874C
题意
有 \(n\) 个城市,\(m\) 条单向道路,每条道路可以从城市 \(a_i\) 走到城市 \(b_i\),保证每条道路都满足 \(a_i < b_i\)。
有两个人一起从 \(1\) 走到 \(n\),如果当前他们在城市 \(u\),他们俩会分别选择一条从城市 \(u\) 出发的未被摧毁的边。
如果两个人选择的边的终点是相同的,他们就会沿着这条边走到下一个城市;否则,这两条道路都将被摧毁。
如果从 \(u\) 连出的所有道路都被摧毁,那么任务失败;如果最终到达城市 \(n\),那么任务成功。
现在,其中的一个人知道另一人会随机选择一条边,但是他会最优选择道路。
求出任务成功的最大概率。
思路
设 \(dp_i\) 表示当前在城市 \(i\) 的任务成功的概率。
很显然,对于 \(u\) 连出去的每一条边,设它们的终点分别为 \(v_1, v_2 \dots, v_k\),那么,我们肯定按照 \(dp_{v_i}\) 从大到小选择,尽量选择成功概率更大的边。
所以我们按照这个顺序考虑 \(u\) 连出去的每一条边。
设 \(p_{i, j}\) 表示当前有 \(i\) 条边,第 \(j\) 条边同时被两个人选中的概率。
很显然的,\(p_{i, 1} = \frac{1}{i}\)。我们考虑其他的情况:
假设当前有 \(i\) 条边,可以明确的一点是被优先选择的一定是第 \(1\) 条边,那么,考虑两种情况:
- 被选中的另一条边在 \(j\) 之前,则选中一条既不是第 \(1\) 条,也不是第 \(j\) 条的边概率就是 \(\frac{j - 2}{i}\),在这种情况下,边的总数就会减少 \(2\),第 \(j\) 条边的编号也会往前挪两位。
- 被选中的另一条边在 \(j\) 之后,则选中这样的边的概率就是 \(\frac{i - j}{i}\),在这种情况下,边的总数还是会减少 \(2\),但是第 \(j\) 条边的编号只会往前挪 \(1\) 位。
所以,转移就是:\(p_{i, j} = \frac{j - 2}{i} \cdot p_{i - 2, j - 2} + \frac{i - j}{i} \cdot p_{i - 2, j - 1}\)。
我们再重新考虑 \(dp\) 数组的转移:\(dp_u = \sum\limits_{i = 1} ^ k dp_{v_i} \cdot p_{k, i}\)。
注意要提前预处理出所有的 \(p_{i, j}\) 即可。
代码
#include <bits/stdc++.h>
using namespace std;
using db = double;
const int N = 5010;
int T, n, m;
db p[N][N], dp[N];
vector<int> g[N];
void Solve() {
cin >> n >> m;
while (m--) {
int u, v; cin >> u >> v;
g[u].push_back(v);
}
dp[n] = 1;
for (int i = n - 1; i >= 1; i--) {
sort(g[i].begin(), g[i].end(), [](int i, int j) {return dp[i] > dp[j];});
int k = g[i].size();
for (int j = 0; j < k; j++) {
dp[i] += dp[g[i][j]] * p[k][j + 1];
}
}
cout << fixed << setprecision(10) << dp[1] << '\n';
fill(dp + 1, dp + n + 1, 0);
for (int i = 1; i <= n; i++) g[i].clear();
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
for (int j = 1; j < N; j++) {
p[j][1] = 1.0 / j;
for (int t = 2; t <= j; t++) {
p[j][t] = (t - 2) * 1.0 / j * p[j - 2][t - 2] + (j - t) * 1.0 / j * p[j - 2][t - 1];
}
}
cin >> T;
while (T--) Solve();
return 0;
}