Loading

概率期望 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\) 相互独立,则:

\[E(XY) = E(x) \cdot E(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\) 个不同的位置,那么转移就是:

\[dp_i = 1 + \frac{i}{n} \cdot dp_{i - 1} + \frac{n - i}{n} \cdot dp_{i + 1} \]

其中,选中不同的位置的概率是 \(\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\) 条边,那么,考虑两种情况:

  1. 被选中的另一条边在 \(j\) 之前,则选中一条既不是第 \(1\) 条,也不是第 \(j\) 条的边概率就是 \(\frac{j - 2}{i}\),在这种情况下,边的总数就会减少 \(2\),第 \(j\) 条边的编号也会往前挪两位。
  2. 被选中的另一条边在 \(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;
}

CF1866M

题意

posted @ 2024-08-06 13:27  chengning0909  阅读(4)  评论(0编辑  收藏  举报