概率和期望
概率和期望
期望
常见技巧与知识
- 如果当前步数通往下一步时,有 的概率原地打转,则走到下一步的期望步数为
- 如果在进行某个操作时达到要求则停止,求期望步数,则可设达到要求后不停止,但不耗步数,保持问题的对称性
- 求步数的期望:
- 套路设计状态:设 为已经走了 步时,走完的期望步数
一些公式:
条件期望:如果 不发生时,随机变量 的值取 ,那么
全期望公式:如果一组事件 满足它们之间恰好有一个会发生,即 ,那么
这在 DP 中很常见
特例:
如果两个变量 独立,即其结果互不影响,,那么
这个时候期望才能乘
注意: 只有是常数时才和自己独立,独立性不传递,即 , 分别互相独立,但 不一定互相独立
期望逆推
现在我还不是很确定为什么期望倒推,有两种说法我觉得都有道理:一是这种期望题末状态未知,但初状态已定,有可能某些末状态根本到不了,顺推的话还要计算到达它的概率,逆推比较方便;二是有人说倒推是为了保证转移时各种情况的概率之和为 ,保证正确性。可能两种原因都有?
T1:P4550 收集邮票
因为邮票的花费与次数相关——要什么转什么,还要转期望次数
套路设计状态: 为已经买到i张邮票,买全n张的期望次数
分2种情况讨论:
-
下次不幸买到已有的i张,概率为 ,还要再用 的次数买
-
幸运的买了没有的 张,概率为 ,用 的次数即可
(本次花了 次买,)
那钱数呢?买 次总钱数为 ,但期望不能这样算
套路设计状态: 为已经买到 张邮票,买全 张的期望总钱数
但我们不清楚已经买 张的总次数,只知道后面的次数
那就把题改一下:购买倒数第 次邮票需要 元(反过来先加后加一样)
还是分情况:
-
下次不幸买到已有的 张,概率为 ,本次花费 元(注意本次以后还有 次,),以后要 元
-
幸运的买了没有的 张,概率为 ,本次花费 元(本次以后还有 次),以后要 元
两边都有 g[i],化简一下:
易知 ,刚开始没邮票,求到 即可
代码:
#include<bits/stdc++.h>
using namespace std;
int n;
double f[10010], g[10010];
int main()
{
scanf("%d", &n);
for(int i = n - 1; i >= 0; i--) f[i] = f[i + 1] + n * 1.0 / (n - i);
for(int i = n - 1; i >= 0; i--)
g[i] = i * 1.0 / (n - i) * f[i] + f[i + 1] + g[i + 1] + n * 1.0 / (n - i);
printf("%.2f", g[0]);
return 0;
}
又一道期望神题
这里只用求次数,状态很神奇:
为从第 个灯要按到有 个灯要按的期望次数
推转移方程:
解释第二种情况:现在有 的概率按到要按的,一次就按到 ,还有 的概率按到不用按的,先从 个按到第 个,再从第 个按到 个(注意一开始按了一个,加 )
第一个很好求,最后加 即可
化简第2个:
注意一下状态结尾不是 ,因为我们总共不一定要 次
求出按几次就好了(注意特判可能总共只要 次及以下)
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n, k, a[100010], fact[100010], inv[100010], mod = 100003, f[100010], ans, sum;
int main()
{
scanf("%lld%lld", &n, &k);
fact[0] = inv[1] = 1;
for(int i = 1; i <= n; i++) scanf("%lld", &a[i]);
for(int i = n; i > 0; i--)
if(a[i])
{
for(int j = 1; j * j <= i; j++)
if(i % j == 0)
{
a[j] ^= 1;
if(j * j != i) a[i / j] ^= 1;
}
sum++;
}
for(int i = 2; i < mod; i++) inv[i] = inv[mod % i] * (mod - mod / i) % mod; // 推逆元公式
for(int i = n; i > 0; i--) f[i] = (((n - i) * f[i + 1] % mod + n) % mod * inv[i]) % mod;
if(sum <= k) ans = sum;
else
{
for(int i = sum; i > k; i--) ans = (ans + f[i]) % mod;
ans = (ans + k) % mod;
}
for(int i = 1; i <= n; i++) ans = ans * i % mod;
printf("%lld", ans);
return 0;
}
设 表示聪聪在 ,可可在 时,聪聪抓到可可的期望步数
先预处理出图中点两两之间的距离
如果 ,显然
让聪聪先靠近可可 步,如果抓到了,
否则
看起来要高斯消元,但复杂度不允许
仔细分析一下,由于聪聪一定会靠近可可,所以可可在 ,聪聪在 时,聪聪不可能回到 ,因为这样就远离了可可
发现转移之间无环,可以直接记忆化搜索,或是建出图拓扑排序
int id(int x, int y) {return (x - 1) * n + y;}
void bfs(int st)
{
memset(dis[st], 0x3f, sizeof(dis[st]));
memset(vis, 0, sizeof(vis));
queue<int> q;
dis[st][st] = 0, q.push(st), vis[st] = 1;
while(!q.empty())
{
int t = q.front(); q.pop();
for(int i : edge[t])
if(!vis[i] && dis[st][i] > dis[st][t] + 1) vis[i] = 1, dis[st][i] = dis[st][t] + 1, q.push(i);
}
}
int step(int x, int y) // x 向 y 走 2 步
{
int mn = inf, lsh = inf;
for(int i : edge[x])
if(dis[i][y] == dis[x][y] - 1 && i < mn) mn = i;
lsh = mn, mn = inf;
for(int j : edge[lsh])
if(dis[j][y] == dis[lsh][y] - 1 && j < mn) mn = j;
return mn;
}
void topsort()
{
queue<int> q;
for(int i = 1; i <= n * n; ++i)
if(!deg[i]) q.push(i);
while(!q.empty())
{
int t = q.front(); q.pop();
lin[++cnt] = t;
for(int i : tu[t])
{
--deg[i];
if(deg[i] == 0) q.push(i);
}
}
}
int main()
{
n = read(), m = read(), a = read(), b = read();
for(int i = 1; i <= m; ++i)
{
u = read(), v = read();
edge[u].pb(v), edge[v].pb(u);
}
for(int i = 1; i <= n; ++i) bfs(i);
for(int i = 1; i <= n; ++i) // 聪聪在 i 可可在 j
for(int j = 1; j <= n; ++j)
{
if(i == j) f[i][j] = 0;
else if(dis[i][j] <= 2) f[i][j] = 1;
else
{
int pos = step(i, j); f[i][j] = -1;
for(int k : edge[j])
tu[id(pos, k)].pb(id(i, j)), ++deg[id(i, j)];
tu[id(pos, j)].pb(id(i, j)), ++deg[id(i, j)];
}
}
topsort();
for(int i = 1; i <= cnt; ++i)
{
int x = (lin[i] - 1) / n + 1, y = (lin[i] - 1) % n + 1;
if(f[x][y] >= 0) continue;
f[x][y] = 0;
int pos = step(x, y);
for(int j : edge[y])
f[x][y] += (f[pos][j] + 1) / (double)(edge[y].size() + 1);
f[x][y] += (f[pos][y] + 1) / (double)(edge[y].size() + 1);
}
printf("%.3lf", f[a][b]);
return 0;
}
结合其它 DP 方法
的范围显然得考虑状压 DP,设 为已经抛了 个礼物,获得礼物的集合为 时剩下得分的最大期望
枚举这一轮的礼物种类,如果 中包含了所有的前置礼物,则决策可以时选它,
否则不能选,
稍微进行剪枝,预处理出合法的集合
int main()
{
k = read(), n = read();
for(int i = 1; i <= n; ++i)
{
p[i] = read(), u = read();
while(u) s[i] |= (1 << (u - 1)), u = read();
}
for(int i = 0; i < (1 << n); ++i)
{
int flag = 1;
for(int j = 1; j <= n; ++j)
if(((i >> (j - 1)) & 1) && (s[j] & i) != s[j]) {flag = 0; break;}
if(flag) ok.pb(i);
}
for(int i = k - 1; i >= 0; --i)
for(int v : ok)
{
for(int j = 1; j <= n; ++j)
{
double nw = -1e9;
if((s[j] & v) == s[j]) nw = max(nw, f[i + 1][v | (1 << (j - 1))] + (double)p[j] * 1.0);
nw = max(nw, f[i + 1][v]);
f[i][v] += nw / (double)n;
}
}
ans = f[0][0];
printf("%.6lf", ans);
return 0;
}
期望线性
期望的线性性很重要!
但是乘积的期望不那么好处理,尤其是相关的时候
通常是大力拆括号/组合意义/结合各种优化来 DP
还有将问题等价去掉限制,有的时候要大胆猜测
CF1842G Tenzing and Random Operations
计算 的期望
拆开的话很复杂, 相乘的期望也不好算
但我们知道展开后一定是若干个 个数乘积的单项式相加
于是算出所有可能的选择方案中这些单项式的总和即可知道期望
考虑有这样的数表:
表示第 次操作,第 个位置是否被增加
每个位置的值为 ,且一行中为 的一定是一个后缀(因为是后缀加)
操作次数很多,但是这个单项式只有 个数
每个位置的 有能不能用,有没有用过两种状态
我们不关心它能不能用,我们只关心它用了没有,而且总共只会用 个
也就是说,没有用到的操作我们不关心它具体的起点,将它们归为一类计算贡献
这样设计 表示已经统计了前 个数,用了 次操作的总和
- 第 个数放 ,
- 第 个数想选之前已用过的操作,此时根据 的性质, 肯定是 ,能用上,有 种选法,
- 第 个数想选之前没用过的操作,必须把这个操作在前面某次就钦定为 ,有 个操作,前面有 个可用起点可供选择,
最后统计答案,用了 次操作时,剩下 次操作可以随便指定起点,共 种方法
期望还要除以 ,化简后
或者直接 DP 期望, 为已经统计了前 个数,用了 次操作的某种摆放方案中单项式的总和的期望,放 期望直接乘 ,放之前用过的,由于 所以一定能成功,期望 ,放没用过的,它有 的概率能成功,否则 ,整个式子值直接为 ,所以
int main()
{
read(n, m, v);
for(ll i = 1; i <= n; ++i) read(a[i]);
f[0][0] = 1;
for(ll i = 1; i <= n; ++i)
{
for(ll j = 0; j <= i && j <= m; ++j)
{
f[i][j] = f[i - 1][j] * ((j * v % mod + a[i]) % mod) % mod;
if(j) f[i][j] = (f[i][j] + f[i - 1][j - 1] * (m - j + 1) % mod * i % mod * v % mod) % mod;
}
}
for(ll i = 0; i <= n && i <= m; ++i) ans = (ans + f[n][i] * qmi(qmi(n, i), mod - 2) % mod) % mod;
printf("%lld", ans);
return 0;
}
首先转化 ,因为只有所有点被选中后才没有点被选,而期望有线性性,可以拆开
这样计算一个点被选中的次数,只跟这个点到根的这条链上的点有关
点 只有当链上所有点被选过后才不会被选, 的深度为 (根深度为 )
但是到根路径上全部染黑后这个点不能再选的限制不好处理
假设我们依然能选这些点,但不计算贡献,将问题变得对称,这是常见的处理手段
考虑随便选择时的操作序列,选这些点并不影响 的期望选择次数,而且也不会影响所有点的颜色,不选这些点就是把这些不合法操作从操作序列中去掉,但没有造成任何影响
因此,原问题与新问题等价
这样就好办了,设当前已选 个点,有 的概率不会选新的,则期望 步选到下一个点
总共期望 步选完这条链
问题转化后,每个点被选的机会均等,所以 期望被选 次
答案为
int main()
{
read(n), dep[1] = 1;
for(int i = 2; i <= n; ++i) read(fa[i]), dep[i] = dep[fa[i]] + 1;
for(int i = 1; i <= n; ++i) h[i] = (h[i - 1] + qmi(i, mod - 2)) % mod;
for(int i = 1; i <= n; ++i) ans = (ans + h[dep[i]]) % mod;
printf("%lld", ans);
return 0;
}
做了这道题,就不再想说 「组合意义天地灭,数学推导保平安」
推式子我至今自己推不出来,CF 官方题解中的也非常复杂
但是可以用组合意义巧妙的解释这题
由于每轮均匀随机,期望抽到的张数相同,因此
根据期望的公式,
因此,
先算一轮中抽的张数,可以大力枚举张数计算概率,但期望有线性性,可以单独拿出一张数字牌考虑它期望被抽的次数,它在所有鬼牌前才能被抽,概率为 ,那么共 张牌,最后还要抽一张鬼牌结束,期望张数为
算轮数比较麻烦,设 为当前还剩 张数字牌没拿到时的剩余期望轮数
由于拿到已经拿到的牌不会结束一轮也不会有贡献,可以直接忽略
所以每轮开头可以看作是新牌或鬼牌,把抽到一张新牌的所有轮记作一回合
但是抽到新牌后,一轮不会结束,有可能再抽到新牌,所以一回合中先去掉抽出新牌的那一轮,最后再加上令游戏结束的最后一轮
这样下一回合的开头紧接着上一回合的结尾,每轮有 的概率开头为鬼牌,因此期望 轮进入下一回合
去掉最后一轮后,
答案即为
int main()
{
cin >> n >> m;
fact[0] = invf[0] = 1, V = n + m;
for(ll i = 1; i <= V; ++i) fact[i] = fact[i - 1] * i % mod;
invf[V] = qmi(fact[V], mod - 2);
for(ll i = V - 1; i > 0; --i) invf[i] = invf[i + 1] * (i + 1) % mod;
for(ll i = 1; i <= n; ++i) h[i] = (h[i - 1] + invf[i] * fact[i - 1] % mod) % mod;
eturn = (m * h[n] % mod + 1) % mod;
for(ll i = 1; i <= n + 1; ++i) estep = (estep + invf[n - i + 1] % mod * fact[n + m - i] % mod * i % mod) % mod;
estep = estep * fact[n] % mod * invf[m + n] % mod * m % mod;
printf("%lld", estep * eturn % mod);
return 0;
}
高次期望
:
拆括号,
考虑一下当新加入位置 时,相对 会产生 的新增贡献,计算它即可
注意这里新增是相对上个位置填完后强制计算后缀的贡献后而言
有 的概率填 0,结束这一段,贡献不新增
有 概率填 1,此时
-
对于 产生 新增贡献
-
对于 产生 新增贡献
-
对于 即答案, 为到第 位的总贡献,产生 新增贡献,但统计的是总贡献,所以还要加上到前一位的总贡献
所以
答案为
小问题:为什么 的新增贡献中没有像 那样加上前一项呢?
其实比较简单,因为对于最终答案来说,新增的是整个 和 ,式子后面加的是对于上一位时新增的 、 来说这一位、 新增的,但对上个 而言只新增了 ,前一项 是到上一位的总和
即相当于拆开了每一位的贡献,把每一位的贡献累加起来
(有点绕)
:
for(reg int i = 1; i <= n; ++i)
{
x1[i] = (x1[i - 1] + 1) * p[i];
x2[i] = (x2[i - 1] + 2 * x1[i - 1] + 1) * p[i];
f[i] = f[i - 1] + (3 * x1[i - 1] + 3 * x2[i - 1] + 1) * p[i];
}
概率
常见公式
一些太基础的就不写了
条件概率公式:
这里的 指事件 在 已经发生时发生的概率
全概率公式:如果一组事件 满足它们之间恰好有一个会发生,即 ,那么
常用的特例:
贝叶斯公式:可以由条件概率公式推导而来,
概率分布
用于表述随机变量取值的概率规律
常见分布:
- 两点分布,又称伯努利分布
只有 两种取值,有 的概率是 , 的概率是
- 几何分布,设在伯努利实验中, 为得到一次成功需要的实验次数,那么
它的期望 ,可以用等比数列求和推,也可以用 DP 推出
场上并不知道分布
但是发现每个位置的期望大小相同为 ,且每个位置独立,答案是
然后由于精度要求不高,暴力算了数列前 项就算出了 ,过了
现在知道分布后,,答案就是
官方题解很妙:看作一排灯,每盏灯有 的概率亮起,序列中每个位置的大小就是第一盏亮着的灯的位置,然后把它后面的那盏灯记为第一盏,以此类推,得到后面位置的大小,那么问题转化为 内期望有多少盏亮着的灯,答案为
- 二项分布,描述进行 次伯努利实验成功的次数,
期望为 ,还是比较好理解的,一次实验成功次数的期望是 ,期望有线性性
- 超几何分布,描述抽样不放回, 个产品中 个不合格,现在抽 个送检,设 为不合格样品数量
则 ,总方案数除以选择方案数,期望 ,证明比较复杂,注意虽然前一次抽取会影响样本空间,但概率权重会把影响抵消
树上处理双向贡献
期望没什么用,实际求的是每个节点有电的概率之和
发现某个节点通电,要么它自己有电,要么它的父节点或子节点有电传给它
如果设 表示 有电的概率,那么发现它要从父节点,子节点同时转移过来,没法做
考虑先只处理子节点,注意子节点 有电与 有电并不互斥,所以要用容斥原理算出 或 通电的概率
这样根节点处的概率已经算对了
然后处理父节点,此时父节点通电的概率就要去掉从当前子树通电的情况
同样用容斥,设 为 父节点不从当前节点通电的概率, 为当前节点到父节点有电的概率,
,解方程得
则 ,仍然是容斥原理计算
细节:如果 ,则可以直接跳过,因为始终有电了,防止除以
#include<bits/stdc++.h>
#define pb push_back
#define mp make_pair
#define fi first
#define se second
using namespace std;
const int N = 500010, SZ = (1 << 14) + 5; const double eps = 1e-9;
typedef pair<int, int> pii;
int n, u, v, w, p[N], fa[N];
vector<pii> edge[N];
double f[N], ans;
static char buf[SZ], *bgn = buf, *til = buf;
char getc()
{
if(bgn == til) bgn = buf, til = buf + fread(buf, 1, SZ, stdin);
return bgn == til ? EOF : *bgn++;
}
int read()
{
char ch = getc(); int x = 0;
while(ch < '0' || ch > '9') ch = getc();
while(ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + ch - '0', ch = getc();
return x;
}
void dfs1(int x, int fath) // 算只从子节点处通电的概率
{
fa[x] = fath, f[x] = (double)p[x] * 1.0 / 100.0;
for(pii y : edge[x])
if(y.fi != fath)
{
dfs1(y.fi, x);
double pr = f[y.fi] * y.se * 1.0 / 100.0;
f[x] += pr - f[x] * pr; // 容斥,因为两个事件不互斥,都发生的概率为 P(A)+P(B)-P(AB)
}
}
void dfs2(int x) // 加上从父节点处通电的概率(初始根节点就是对的)
{
for(pii y : edge[x])
{
if(y.fi == fa[x]) continue;
double pr = f[y.fi] * y.se / 100.0;
if(fabs(pr - 1.0) > eps)
{
double pa = (f[x] - pr) / (1.0 - pr); // x不从 y处通电的概率
f[y.fi] += pa * y.se / 100.0 - f[y.fi] * pa * y.se / 100.0;
}
dfs2(y.fi);
}
}
int main()
{
n = read();
for(int i = 1; i < n; ++i)
{
u = read(), v = read(), w = read();
edge[u].pb(mp(v, w)), edge[v].pb(mp(u, w));
}
for(int i = 1; i <= n; ++i) p[i] = read();
dfs1(1, 0), dfs2(1);
for(int i = 1; i <= n; ++i) ans += f[i];
printf("%.6lf", ans);
return 0;
}
轮数处理
把期望拆成每张卡牌发动的概率 分数,实际上是求概率
一轮一轮直接求肯定不好求,考虑从整体计算
如果有 轮决策到了卡牌 ,则都不发动的概率为 ,发动的概率为
但是如果一轮中前面的卡牌有发动的,这一轮就轮不到它
设 为前 张卡牌,发动了 张的概率
则第 张有 轮轮到了它决策,分它发动和不发动两种情况转移
设 为第 张卡牌发动的总概率,则
int main()
{
ios::sync_with_stdio(false), cin.tie(0);
cin >> t;
while(t--)
{
memset(f, 0, sizeof(f)), memset(pw, 0, sizeof(pw));
cin >> n >> r, ans = 0;
for(int i = 1; i <= n; ++i) cin >> p[i] >> a[i], s[i] = 0, pw[i][0] = 1;
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= r; ++j) pw[i][j] = pw[i][j - 1] * (1 - p[i]); // 第 i张卡在 j轮中不发动的概率
f[0][0] = 1; // f[i][j]:前 i张,有 j张发动了技能的概率
for(int i = 1; i <= n; ++i)
{
for(int j = 0; j <= i && j <= r; ++j) // 如果某一轮前面有发动的卡,那么这一轮它一定不会发动
{
if(j) f[i][j] = f[i - 1][j] * pw[i][r - j] + f[i - 1][j - 1] * (1 - pw[i][r - j + 1]);
else f[i][j] = f[i - 1][j] * pw[i][r - j];
s[i] += f[i - 1][j] * (1 - pw[i][r - j]);
}
}
for(int i = 1; i <= n; ++i) ans += a[i] * s[i];
printf("%.10Lf\n", ans);
}
return 0;
}
杂项
很 NB 的概率题
一看可以覆盖实数,显然不好做
但线段长度均为整数,如果我们知道了它们小数部分的相对大小,则只用知道覆盖到哪个整数就能知道是否覆盖
很小,我们完全可以 枚举相对大小,排序后 DP
注意这里断环为链必须选取最长的放在起点,避免某条线段在最后时转了一圈完全覆盖起点
那么每个整数相当于拆成了 个数,第 个必须放在对应小数部分相对大小对应的点
设 为集合 中的线段,覆盖了 的方案数
转移则先从小到大枚举下一条线段放置的位置,强制从前往后放,避免顺序问题,然后枚举覆盖到的位置和已经放置的集合,状压 DP
最后放置的总方案数为 ,因为除了第一个,共有 种大小顺序,其它线段都有 种放置方法
int main()
{s
cin >> n >> c;
for(int i = 1; i <= n; ++i) cin >> a[i], id[i] = i;
sort(a + 1, a + n + 1, [](const int x, const int y){return x > y;});
do
{
memset(f, 0, sizeof(f)); // f[i][s]: cover [0,i], use arcs in state s
f[a[id[1]] * n][1] = 1, ++cnt; // the longest arc must be in the beginning
for(int i = 1; i < n * c; ++i) // place one in i
{
int nw = i % n + 1;
if(nw == 1) continue;
for(int j = i; j <= n * c; ++j)
for(int k = 1; k < (1 << n); k += 2)
if(!(k >> (nw - 1) & 1))
f[min(n * c, max(j, i + a[id[nw]] * n))][k | (1 << (nw - 1))] += f[j][k];
}
ans += f[n * c][(1 << n) - 1];
}while(next_permutation(id + 2, id + n + 1));
printf("%.16Lf", (long double)ans / (long double)cnt / (long double)pow((long double)c, n - 1));
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】