*【学习笔记】(20) 期望与概率
概率
1.基本概念
2.古典概率
3.条件概率
3.1 乘法公式
3.2 全概率公式
3.3 贝叶斯公式
期望
1.期望的线性性质
2.例题
Ⅰ. P4206 [NOI2005] 聪聪与可可
\(p_{x,y}\) 表示顶点 \(i\) 到顶点 \(j\) 的最短路上与顶点 \(i\) 相邻且编号最小的顶点编号。
\(f_{x,y}\) 表示聪聪在 \(x\), 可可在 \(y\) 时,聪聪抓住可可的平均步数。
\(yy\) 表示可可下一步所取的点。
\(d_x\) 表示 \(x\) 点的度数。
用记忆化搜索来实现:
- \(\Large x = y\), \(\Large f_{x,y} = 0\)
- \(\Large p_{x,y} = y \ \ || \ \ p_{p_{x,y},y} = y\) ,\(\Large f_{x,y} = 1\)
- \(\Large f_{x,y} = \dfrac{\sum\limits_{w} f_{p_{p_{x,y}, w}}}{d_y + 1} + 1\)
点击查看代码
#include<bits/stdc++.h>
#define db double
using namespace std;
const int N = 1e3 + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, m, s, t;
int tot, Head[N], Next[N << 1], to[N << 1], edge[N << 1];
bool vis[N];
db f[N][N];
int p[N][N], dis[N][N], d[N];
void add(int u, int v, int w){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot, edge[tot] = w, ++d[u];
}
void spfa(int s){
memset(dis, -1, sizeof(dis));
vis[s] = 1, dis[s][s] = 0;
queue<int> q; q.push(s);
while(!q.empty()){
int x = q.front(); q.pop();
vis[x] = 0;
for(int i = Head[x]; i; i = Next[i]){
int y = to[i], z = edge[i];
if(dis[s][y] == -1 || (dis[s][y] == dis[s][x] + z && p[s][x] < p[s][y])){
dis[s][y] = dis[s][x] + z;
if(p[s][x]) p[s][y] = p[s][x];
else p[s][y] = y;
if(!vis[y]) q.push(y), vis[y] = 1;
}
}
}
}
db dfs(int x, int y){
if(f[x][y]) return f[x][y];
if(x == y) return f[x][y] = 0.0;
if(p[x][y] == y || p[p[x][y]][y] == y) return f[x][y] = 1.0;
for(int i = Head[y]; i; i = Next[i]) f[x][y] += dfs(p[p[x][y]][y], to[i]);
f[x][y] = (f[x][y] + dfs(p[p[x][y]][y], y)) / (d[y] + 1) + 1;
return f[x][y];
}
int main(){
n = read(), m = read(), s = read(), t = read();
for(int i = 1; i <= m; ++i){
int u = read(), v = read();
add(u, v, 1), add(v, u, 1);
}
for(int i = 1; i <= n; ++i)
spfa(i);
printf("%.3lf\n", dfs(s, t));
return 0;
}
Ⅱ. P4745 [CERC2017]Gambling Guide
假设当前处理到 \(x\) 点, 期望为 \(f_x\)。
发现如果 \(x\) 周围点 \(f_y < f_x\) 的话, \(f_y\) 是会对 \(f_x\) 有影响的。
有什么影响呢?即 \(y\) 下一步走到 \(x\)。
从 \(n\) 开始遍历,已经遍历过的点 \(f\) 一定小于 \(f_x\),没有遍历过的点 \(f\) 一定大于 \(f_x\)。
为什么我们可以保证这个性质?因为我们试着考虑最短路(\(\text{Dijkstra}\)),会发现每一个已经遍历过的点与根的距离一定小于一个没有遍历过的点,这是它的正确性。
假设所有 \(f_i<f_x(1\leqslant i\leqslant n)\) 中 \(i\) 构成的点集称为 \(S\),点集大小为 \(c\)。
那么走到点 \(x\) 时肯定走 \(S\) 中的点是最优的。
得到式子:
化简得:
点击查看代码
#include<bits/stdc++.h>
#define db double
#define mp make_pair
using namespace std;
const int N = 3e5 + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, m;
int tot, Head[N], Next[N << 1], to[N << 1], d[N], cnt[N];
db f[N], sum[N];
bool vis[N];
priority_queue<pair<double, int> > pq;
void add(int u, int v){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot, ++d[u];
}
void dijkstra(int s){
pq.push(mp(0, s));
while(!pq.empty()){
int x = pq.top().second; pq.pop();
if(vis[x]) continue; vis[x] = 1;
for(int i = Head[x]; i; i = Next[i]){
int y = to[i]; if(vis[y]) continue;
++cnt[y], sum[y] += f[x];
f[y] = (d[y] + sum[y]) / cnt[y];
pq.push(mp(-f[y], y));
}
}
}
int main(){
n = read(), m = read();
for(int i = 1; i <= m; ++i){
int u = read(), v = read();
add(u, v), add(v, u);
}
dijkstra(n);
printf("%.10lf\n", f[1]);
return 0;
}
Ⅲ. P2221 [HAOI2012]高速公路
Ⅳ. P4284 [SHOI2014] 概率充电器
树形 dp + 期望。
显然节点 \(i\) 通电有三种可能
1.它自己来电了
2.它的子树里有一个点来电了传了过来
3.它的子树外面有一个点来电了传了过来
两次 dfs 即可。
点击查看代码
#include<bits/stdc++.h>
#define db double
#define eps 1e-7
using namespace std;
const int N = 5e5 + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n;
int tot, Head[N], to[N << 1], Next[N << 1], edge[N << 1];
int a[N];
db f[N], ans;
void add(int u, int v, int w){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot, edge[tot] = w;
}
void dfs1(int x, int fa){
for(int i = Head[x]; i; i = Next[i]){
int y = to[i]; if(y == fa) continue;
dfs1(y, x);
db pa = f[y] * (0.01 * edge[i]);
f[x] = f[x] + pa - f[x] * pa;
}
}
void dfs2(int x, int fa){
ans += f[x];
for(int i = Head[x]; i; i = Next[i]){
int y = to[i]; if(y == fa) continue;
db pb = f[y] * (0.01 * edge[i]);
if(pb + 1e-7 > 1.0 && pb - 1e-7 < 1.0){
dfs2(y, x); continue;
}
db pa = (f[x] - pb) / (1 - pb) * (0.01 * edge[i]);
f[y] = f[y] + pa - f[y] * pa;
dfs2(y, x);
}
}
signed main(){
n = read();
for(int i = 1; i < n; ++i){
int u = read(), v = read(), w = read();
add(u, v, w), add(v, u, w);
}
for(int i = 1; i <= n; ++i) a[i] = read(), f[i] = a[i] * 0.01;
dfs1(1, 0), dfs2(1, 0);
printf("%.6lf\n", ans);
return 0;
}
Ⅴ. P2473 [SCOI2008] 奖励关
dp 式子不难想到,就是关于为什么要逆推,以下是我自己的理解:
1.首先顺推过来的话,因为求的是期望,我们要知道情况数,但由于有些情况是不存在的,所以情况数无法知道,所以只能逆推。
2.有后效性
点击查看代码
#include<bits/stdc++.h>
#define db double
using namespace std;
const int N = 1e2 + 67, M = (1 << 15) + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int K, n;
int p[N], sta[N];
db f[N][M];
signed main(){
K = read(), n = read();
for(int i = 1, x; i <= n; ++i){
p[i] = read();
while(x = read()) sta[i] |= (1 << x - 1);
}
for(int i = K; i; --i){
for(int j = 0; j < (1 << n); ++j){
for(int k = 1; k <= n; ++k)
if((j & sta[k]) == sta[k]) f[i][j] += max(f[i + 1][j], f[i + 1][j | (1 << k - 1)] + p[k]);
else f[i][j] += f[i + 1][j];
f[i][j] /= n;
}
}
printf("%.6lf\n", f[1][0]);
return 0;
}
Ⅵ. P3750 [六省联考 2017] 分手是祝愿
先求出至少需要按多少个灯。
显然每个灯最多按一次,且每一个灯按去的效果都是独一无二的,所以直接从大到小按就行了。
设 \(f_i\) 表示从 \(i + 1\) 个需要按的灯到 \(i\) 个需要按的灯的 按的次数的期望。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod = 1e5 + 3, N = 1e5 + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, k, cnt;
int a[N];
ll f[N];
ll qsm(ll a, int b){
ll res = 1;
for(; b; b >>= 1, a = a * a % mod) if(b & 1) res = res * a % mod;
return res;
}
signed main(){
n = read(), k = read();
for(int i = 1; i <= n; ++i) a[i] = read();
for(int i = n; i; --i){
if(!a[i]) continue;
++cnt;
for(int j = 1; j * j <= i; ++j)
if(i % j == 0){
a[j] ^= 1;
if(j * j != i) a[i / j] ^= 1;
}
}
for(int i = n; i; --i){
ll tmp = ((ll)(n - i) * f[i + 1] + n) % mod;
tmp = tmp * qsm(i, mod - 2) % mod;
f[i] = tmp;
}
ll ans = 0;
if(cnt <= k) ans = cnt;
else{
for(int i = cnt; 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\n", ans);
return 0;
}
Ⅶ. P3830 [SHOI2012]随机树
- 考虑第一问
设 \(g[i]\) 表示有 \(i\) 个叶子的二叉树的期望叶子平均深度。
则 \(i-1\) 个叶子的二叉树进行扩展后,叶节点数量变为 \(i\),叶子深度之和增加\(2\)。
故有 \(g[i]=g[i-1]+2/i,g[1]=0\)。
即 \(\displaystyle\sum_{i=2}^n\displaystyle\frac{2}{i}.\)
-
考虑第二问
-
先证明一个小结论:设扩展\(i-1\)次后,左子树有 \(k\) 个叶子的概率、右子树有 \(i-k\) 个叶子的概率为 \(P(k)\)。则有 \(\forall k_1,k_2\in[1,i-1],P(k_1)=P(k_2)\)。
将扩展过程表示为序列,则其中 “扩展左子树” 有 \(k-1\) 个,“扩展右子树” 有 \(i-k-1\) 个,序列的形式数量为:
再考虑 \(k-1\) 次在左子树扩展形成的树的形态数,第 \(i\) 次可有 \(i\) 个叶节点可供扩展,故形态数为 \((k-1)!\)。
同理,右子树的形态数有 \((i-k-1)!\)。
故考虑扩展先后、树的形态,生成一个左子树有 \(k\) 个叶节点、右子树有 \(i-k\) 个叶节点的树的方案数为
与 \(k\) 无关,即 \(P(k_1)=P(k_2)\)。
- 前置芝士:整数概率公式
正整数随机变量 \(x\) 的期望值为:
证明:
\[E(x)=\sum_{i=1}^{\infty}P(x=i)\cdot i=\sum_{i=1}^{\infty}(P(x\geqslant i)-P(x\geqslant i+1))\cdot i \]
\[=\sum_{i=1}^{\infty}P(x\geqslant i)\cdot i-P(x\geqslant i)\cdot(i-1)=\sum_{i=1}^{\infty}P(x\geqslant i) \]
- 计算概率
设 \(f[i][j]\) 为树有 \(i\) 个叶节点,深度不小于 \(j\) 的概率,考虑枚举左子树叶节点数量为 \(k\),有
解释如下,首先考虑左、右子树深度不小于 \(j\) 的概率之和。再考虑左右子树深度皆大于等于 \(j\) 的情况计算了两次,减去一倍即可。
前文我们证明过,左子树叶节点数为 \(1,2,...,i-1\) 的概率相同,即均为 \(1/(i-1)\)。
初始条件,\(f[i][0]=1\),因为无论有多少个叶子,深度大于等于 \(0\) 的概率总为 \(1\)。
由整数概率公式可知,期望深度 \(x\) 为:
摘自 https://www.luogu.com.cn/blog/emptyset/solution-p3830
点击查看代码
#include<bits/stdc++.h>
#define db double
using namespace std;
const int N = 1e2 + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int opt, n;
db ans, f[N][N];
void solve1(){
for(int i = 2; i <= n; ++i) ans += 2.0 / i;
}
void solve2(){
for(int i = 1; i <= n; ++i) f[i][0] = 1;
for(int i = 2; i <= n; ++i)
for(int j = 1; j < i; ++j)
for(int k = 1; k < i; ++k)
f[i][j] += (f[k][j - 1] + f[i - k][j - 1] - f[k][j - 1] * f[i - k][j - 1]) / (i - 1);
for(int i = 1; i < n; ++i) ans += f[n][i];
}
signed main(){
opt = read(), n = read();
if(opt & 1) solve1();
else solve2();
printf("%.6lf\n", ans);
return 0;
}