概率期望 dp

概率期望 dp#

概念#

期望,是每次可能结果的概率乘上结果的权值的总和。

对于一个变量 X,如果它有 n 种取值,第 i 种取值 xi 的概率是 P(xi),那么它的期望就是 E(X)=i=1nxiP(xi)

举个例子:如果你有一个骰子,每次可以扔出 16 中的任意一个数,并且取得每个数的概率是相同的,那么它的取值的期望就是 E(X)=16×1+16×2+16×3+16×4+16×5+16×6=72

性质#

线性性#

若随机变量 X,Y 的期望存在,则:

  • 对于任意实数 a,b,都有 E(aX+b)=aE(X)+b
  • E(X+Y)=E(X)+E(Y)

随机变量乘积的期望#

若随机变量 X,Y 的期望存在且 X,Y 相互独立,则:

E(XY)=E(x)E(Y)


综上,当随机变量 X,Y 的期望存在且 X,Y 相互独立时,对于任意实数 a,bE(aX+bY)=aE(X)+bE(Y)

概率期望 dp#

通常在求解达到某一目标的期望代价时,我们并不知道最终代价,因此,通常我们采用 倒序 的方式进行 dp。

Gym-105284C#

题意#

有一个由 n 个结点组成的链,对于每个结点 i,有 1i 的概率保留它,还有 11i 的概率删除它,求出这之后的连通块的期望数量,答案对 109+7

思路#

我们先画出一条链:

我们考虑连通块的数量变化。

  • 当图中的点的数量 +1 时,连通块的数量也 +1
  • 当图中的边的数量 +1 时,连通块的数量 1

我们再写出每个点和每条边保留的概率,对于点 i,假设前 i1 个点已经考虑完了,则点 i 可以形成新连通块的概率就是点 i 保留的概率减去 i1i 的边保留的概率,也就是 1i1i1i1

代码#

#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,将字符 ai 取反。

求出第一次使得 a,b 相等的操作次数。

思路 1#

dpi 表示当前剩下 i 个不同的位置,那么转移就是:

dpi=1+indpi1+nindpi+1

其中,选中不同的位置的概率是 in,操作后就只剩下 i1 个不同的位置了。

而选中原本已经相同的位置的概率是 nin,操作后就有 i+1 个不同的位置。

初始状态 dp0=0

但是这种转移是不存在拓扑序的,状态 i 既会从状态 i1 转移过来,也会从状态 i+1 转移过来。

我们设 dp1=x,n=4,然后用解方程的方式将每个 dpi 表示成 kx+b 的形式,那么

dp1=1+14dp0+34dp2=1+140+34dp2,可以解出 dp2=43(x1)=43x43

dp2=1+24dp1+24dp3=1+24x+24dp3,可以解出 dp3=53x143

dp3=1+34dp2+14dp4=1+34(43x43)+14dp4,可以解出 dp4=83x563

又有 dp4=1+dp3=1+53x143,可以解出 x=15

ab 中不同的字符数量为 cnt,我们就可以将解出来的 x 代入 dpcnt,算出答案。

代码 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#

costi 表示从剩余 i 个不同位置转移到剩余 i1 个不同位置需要的次数的期望。

这种时候转移有两种情况:

  • 修改的位置恰好不同,概率是 in
  • 修改的位置是相同的,那么不同的位置的数量在操作后就会变成 i+1 个,概率是 nin,那么之后还需要从 i+1 转移到 i,再转移到 i1

因此,我们的转移是:costi=nin(1+costi+1+costi)+in

移项后就会变成:costi=(ni)costi+1+ni=(ni1)costi+1+ni

代码 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,当它尝试从高度 i1 爬到高度 i 时有 pi 的概率会掉回高度 0 的位置,求爬到高度 n 所经过的时间的期望值。

思路#

我们考虑上个例题的思路 1,还是令 dpi 表示从 i 爬到 n 的期望时间。

那么转移就是:dpi=pi+1dp0+(1pi+1)dpi+1+1

初始状态为 dpn=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 的祖先一共有 depu 个,那么,我们考虑 u 在操作序列中排在所有祖先的前面的概率。

我们又可以发现,这个概率只与 uu 的祖先有关,与其他 ndepu1 个点并没有关系。

所以,我们考虑在 depu+1 个点中,点 u 排在第一个的概率,这个就是 1depu+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 个位置。

我们需要重复一个操作,直到队伍中只剩下一个人,每次操作有 12 的概率让队伍中的第一个人离开,又有 12 的概率让他走到队伍末尾。

对于每一个 i (1in),求出第 i 个人最终留在队伍里的期望。

思路#

我们设 dpi,j 表示还剩 i 个人时,第 j 个人最终留下的期望。

很显然,初始状态为 dp1,1=1

对于其他的状态,则有 dpi,j=12(dpi,(j1)modn+dpi1,j1)

其中,我们有 12 的概率将第一个人移到最后,那么第 j 个人的编号就会变小一个。

同样的,还有另外 12 的概率让第一个人离开,那么第 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,db,你不会受到伤害,但是盾牌的耐久度会减少 1

  • 如果 a>0,d<b,则什么都不会发生。

你将会按照某种随机的顺序对抗怪物,对于每一个 i (1im),请求出当你持着盾牌 i 时,你受到的伤害的期望。

思路#

首先,我们会发现,对于一个盾牌 i,如果它的耐久度为 a,防御等级为 b,那么实际上所有怪物只分为两种:一种是可以使耐久度减少的,另一种是不能使耐久度减少的。

假设对于盾牌 i,有 x 只怪物可以使它的耐久度减少,我们考虑对于每只怪物,它的贡献:

首先,如果 x<a,自然是不需要考虑的,耐久度不会掉完。

那么,对于 xa 的情况,对于第 k 只怪物,如果想要造成伤害,就需要在 k 之前有 a 只可以减少耐久度的怪物发动攻击。

分为两种情况:

  • 如果 dkb,这样的概率是 xax
  • 如果 dk<b,概率是 xa+1x+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 条单向道路,每条道路可以从城市 ai 走到城市 bi,保证每条道路都满足 ai<bi

有两个人一起从 1 走到 n,如果当前他们在城市 u,他们俩会分别选择一条从城市 u 出发的未被摧毁的边。

如果两个人选择的边的终点是相同的,他们就会沿着这条边走到下一个城市;否则,这两条道路都将被摧毁。

如果从 u 连出的所有道路都被摧毁,那么任务失败;如果最终到达城市 n,那么任务成功。

现在,其中的一个人知道另一人会随机选择一条边,但是他会最优选择道路。

求出任务成功的最大概率。

思路#

dpi 表示当前在城市 i 的任务成功的概率。

很显然,对于 u 连出去的每一条边,设它们的终点分别为 v1,v2,vk,那么,我们肯定按照 dpvi 从大到小选择,尽量选择成功概率更大的边。

所以我们按照这个顺序考虑 u 连出去的每一条边。

pi,j 表示当前有 i 条边,第 j 条边同时被两个人选中的概率。

很显然的,pi,1=1i。我们考虑其他的情况:

假设当前有 i 条边,可以明确的一点是被优先选择的一定是第 1 条边,那么,考虑两种情况:

  1. 被选中的另一条边在 j 之前,则选中一条既不是第 1 条,也不是第 j 条的边概率就是 j2i,在这种情况下,边的总数就会减少 2,第 j 条边的编号也会往前挪两位。
  2. 被选中的另一条边在 j 之后,则选中这样的边的概率就是 iji,在这种情况下,边的总数还是会减少 2,但是第 j 条边的编号只会往前挪 1 位。

所以,转移就是:pi,j=j2ipi2,j2+ijipi2,j1

我们再重新考虑 dp 数组的转移:dpu=i=1kdpvipk,i

注意要提前预处理出所有的 pi,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#

题意#

作者:cn

出处:https://www.cnblogs.com/chengning0909/p/18344962

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   chengning0909  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示