「算法学习」概率与期望
「离散概率初步」
连续抛 \(3\) 次硬币,恰好有两次正面的概率是多少?用 \(1\) 和 \(0\) 来表示正面和反面。则一共有 \(8\) 种可能的情况:\(111,110,101,100,011,010,001,000\)。用专业术语来说,这 \(8\) 种情况的集合称为 样本空间。所求的是 "恰好有两次正面" 这个事件的概率。这个事件可以被表示为 \(\{110,101,011\}\),其概率为 \(\dfrac{3}{8}\)。
条件概率。 公式如下 \(P(A|B)=\dfrac{P(AB)}{P(B)}\)。\(P(A|B)\) 是指,事件 \(B\) 所发生的前提下,事件 \(A\) 发生的概率,而 \(P(AB)\) 是指两个时间同时发生的概率。
贝叶斯公式。 \(P(A|B)=P(B|A)\times \dfrac{P(A)}{P(B)}\)。
全概率公式。 计算概率的一种常用方法是:样本空间 \(S\) 分成若干个不相交的部分 \(B_1,B_2,\dots,B_n\),则 \(P(A)=P(A|B_1)\times P(B_1)+P(A|B_2)\times P(B_2)+\dots +P(A|B_n)\times P(B_n)\)。
公式看上去复杂,其实可以形象理解。比如 lwz 同学今天颓原神,星铁,PVZ,不颓的概率分别为 \(0.35\)、\(0.25\)、\(0.4\)、\(0\),被 wx 抓到的概率分别为 \(0\)、\(0\)、\(0.05\)、\(0\) ,那么今天 lwz 被抓到的概率就为 \(0.35\times 0+0.25\times 0+0.4\times 0.05+0\times 0=0.02\)。这也就是 lwz 被 wx 抓到的次数很少的原因。
例题 1:决斗 UVA1636
题目大意:一把手枪,你,与对手 Hitler。Hitler 开了一枪,没死。该你了,你是选择直接开一枪,还是随机转一下再开一枪。(即问哪种方案存活概率大)
直接开一枪没子弹的概率是一个条件概率,因为你的对手已经开了一枪了,等于子串 \(00\) 的个数除以 \(01\) 和 \(00\) 的总数(也就是 \(0\) 的个数)。随机转一下没子弹的概率等于 \(0\) 的比率。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
char a[N];
int sum1, sum2;
int main() {
while (cin >> (a + 1)) {
sum1 = sum2 = 0;
int n = strlen(a + 1);
a[n + 1] = a[1];
for (int i = 1; i <= n; i++) {
if (a[i] == '0') sum1++;
if (a[i] == '0' && a[i + 1] == '0') sum2++;
}
if (sum2 * n > sum1 * sum1) puts("SHOOT");
else if (sum2 * n < sum1 * sum1) puts("ROTATE");
else puts("EQUAL");
}
}
例题 2:奶牛与轿车 UVA10491
题目大意:
车门指门后有车,牛门指门后有牛。表面上看门都一样。从 \(a\) 个牛门,\(b\) 个车门选一道门,你做出了第一次选择,不知道选择的是什么。选择后主持人会替你打开另外的 \(c\) 个牛门,且强制你再选一次门(
没有被自己选过也没有被主持人打开的门),问你选到车门的概率。
思路
分两种情况讨论就行:
-
若第一次选的是牛门。由于他强制要你换门,那么如果要赢那么换的必须是车门。概率为 \(\dfrac{a}{a+b}\times \dfrac{b}{a+b-c-1}\)。
-
第一次选择是车门。同理的,换的门也一定要是车门。概率为 \(\dfrac{b}{a+b}\times \dfrac{b-1}{a+b-c-1}\)。
将上述两种情况相加即可,答案为 \(\dfrac{a}{a+b}\times \dfrac{b}{a+b-c-1}+\dfrac{b}{a+b}\times \dfrac{b-1}{a+b-c-1}\)。
代码
本题目保留 \(5\) 位小数。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5 + 5;
double a, b, c;
signed main() {
while (cin >> a >> b >> c) {
printf("%.5lf\n", a / (a + b) * b / (a + b - c - 1) + b / (a + b) * (b - 1) / (a + b - c - 1));
}
}
例题 3:条件概率 UVA11181
题目大意
有 \(n\) 个人要去买东西,第 \(i\) 个人买到东西的概率为 \(p_i\)。现在已知恰好有 \(r\) 个人买了东西,在这种条件下,求每个人买到东西的概率。本题有多组数据,满足测试数据组数不超过 \(50\)。
对于每组测试数据,共 \(n+1\) 行输入。第一行输入两个整数 \(n,r\)。第 \(2\) 到 \(n+1\) 行中第 \(i\) 行输入 \(p_{i-1}\)。输入以
0 0结束。输出格式:对于每组测试数据,输出 \(n+1\) 行。第一行先输出
Case i,其中 \(i\) 为当前测试数据的编号。后面 \(n\) 行中第 \(i\) 行输出第 \(i\) 个人买到东西的概率,保留六位小数。满足 \(1\le n\le 20,0\le r\le n,0.1<p_i<1\)。
思路
\(r\) 个人买了东西 这个事件叫 \(E\),第 \(i\) 个人买东西 这个事件叫 \(E_i\),则要求的是条件概率 \(P(E_i|E)\)。根据条件概率公式,\(P(E_i|E)=\dfrac{P(E_iE)}{P(E)}\)。
考虑到 \(n\leq20\),于是二进制枚举出每个人是否买东西的情况,算出 \(P(E_iE)\) 和 \(P(E)\) 即可。
代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 21;
int T, n, r;
double res;
double p[N], sum[N];
signed main() {
while (cin >> n >> r) {
T++; res = 0;
if (n == 0 && r == 0) break;
for (int i = 0; i < n; i++) cin >> p[i], sum[i] = 0;
for (int i = 0; i < (1 << n); i++) {
if (__builtin_popcount(i) != r) continue;
double ans = 1;
for (int j = 0; j < n; j++) {
if (i & (1 << j)) ans = ans * p[j];
else ans = ans * (1 - p[j]);
}
for (int j = 0; j < n; j++) {
if (i & (1 << j)) sum[j] += ans;
}
res += ans;
}
printf("Case %d:\n", T);
for (int i = 0; i < n; i++) printf("%.6lf\n", sum[i] / res);
}
}
「数学期望」
数学期望。 简单的说,随机变量 \(X\) 的数学期望 \(EX\) 就是所有值按照概率加权的和。例如,一个随机变量有 \(\dfrac{1}{2}\) 的概率等于 \(1\),有 \(\dfrac{1}{3}\) 的概率等于 \(2\),有 \(\dfrac{1}{6}\) 的概率等于 \(3\)。则这个随机变量的数学期望为 \(1\times \dfrac{1}{2}+2\times \dfrac{1}{3}+3\times \dfrac{1}{6}=\dfrac{5}{3}\)。
期望的线性性质。 有限个随机变量之和的数学期望等于每个随机变量的数学期望之和。例如,对于两个随机变量 \(X\) 和 \(Y\),\(E(X+Y)=EX+EY\)。
全期望公式。 类似于全概率公式,把所有情况分为若干类,每类计算数学期望,然后把这些数学期望按照每类的概率加权求和。
例题 4:过河 UVA12230
在本题中,过每条河的时间为 \(\dfrac{L}{v}\) 到 \(\dfrac{3L}{v}\) 均匀分布,因此期望过河时间为 \(\dfrac{2L}{v}\)。运用期望的线性性质,把所有 \(\dfrac{2L}{v}\) 加起来,再加上所有在陆地上行走的时间即可。
代码
#include <bits/stdc++.h>
using namespace std;
int n, d, T;
int main() {
while (cin >> n >> d) {
T++;
if (n == 0 && d == 0) break;
double res = d;
for (int i = 1; i <= n; i++) {
double p, l, v;
cin >> p >> l >> v;
res -= l;
res += 2 * l / v;
}
printf("Case %d: %.3lf\n\n", T, res);
}
}
例题 5:UVA1639 糖果
这道题就是典型的根据数学期望定义计算的题目。
思路
考虑最后是第一个盒子还是第二个盒子空掉。
- 如果是第一个盒子空掉。那么第一个盒子肯定被选择了 \(n+1\) 次,在第 \(n+1\) 次的时候发现空了。枚举第二个盒子选择了 \(i\) 次,那么这种情况出现的概率为 \(p^{n+1}\times (1-p)^i\times \dbinom{n+i}{i}\),这种情况第二个盒子剩的糖数为 \(n-i\),那么当前随机变量期望值为 \(p^{n+1}\times (1-p)^i\times \dbinom{n+i}{i}\times (n-i)\)。
- 如果是第二个盒子空掉,枚举第一个盒子选择了 \(i\) 次,同理可得期望值为 \(p^i\times (1-p)^{n+1}\times \dbinom{n+i}{i}\times (n-i)\)。
但是直接计算会出现严重的问题,数据范围 \(n\leq 2\times 10^5\),则 \(\dbinom{n+i}{i}\) 非常大,而 \(p^i,(1-p)^{n+1}\) 非常小。这三项相乘精度损失极大。
于是考虑取对数来解决,第一个盒子空掉的期望值取对数为 \(v_1=\ln (p)\times (n+1)+\ln (1-p) \times i+\ln \dbinom{n+i}{i}+\ln (n-i)\),则对应数学期望为 \(e^{v_1}\)。第二个盒子空掉情况同理,不再赘述。这种方法只用到了普通浮点数相加相乘,就算有乘方形式出现,底数也为 \(e\),只会越乘越大,不会出现像 \(p^i\) 非常小的情况。
你也许会问怎么取一个组合数的对数,我们只需要将组合数拆成定义形式 \(\dfrac{n!}{m!(n-m)!}\) ,预处理阶乘的对数就行。
代码
注意了,处理阶乘对数的数组需要开 long double。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 5;
int T, n;
double p;
long double fac[N];
signed main() {
for (int i = 1; i <= N - 5; i++) fac[i] = fac[i - 1] + log(i);
while (cin >> n >> p) {
T++;
double res = 0;
for (int i = 0; i <= n; i++) {
res += exp((n + 1.0) * log(p) + i * log(1.0 - p) + fac[n + i] - fac[i] - fac[n] + log(n - i));
res += exp((n + 1.0) * log(1.0 - p) + i * log(p) + fac[n + i] - fac[i] - fac[n] + log(n - i));
}
printf("Case %d: %.6lf\n", T, res);
}
}
例题 6:优惠券 UVA10288
若当前收集到了 \(k\) 个图形,下一次买票收集到新图形的概率为 \(\dfrac{n-k}{n}\),则下次收集到新图形还需要 \(\dfrac{n}{n-k}\) 次买票。
对于所有 \(k\) 从 \(0\) 至 \(n-1\),总共需要 \(\dfrac{n}{n}+\dfrac{n}{n-1}+\dots+\dfrac{n}{1}=n\times (\dfrac{1}{n}+\dfrac{1}{n-1}+\dots+\dfrac{1}{1})\) 次买票。
但这道题需要化为分数形式,所以我们求出 \(1\) 到 \(n\) 的 lcm 来通分即可。
备注:这道题也可以用期望 dp 来做,放后面细说。
代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5 + 5;
int n;
int lcm(int a, int b) {
return a * b / __gcd(a, b);
}
int get(int x) { // 得到x的位数
int res = 0;
while (x) {
res++;
x /= 10;
}
return res;
}
signed main() {
while (cin >> n) {
int res = lcm(1, 2);
for (int i = 3; i <= n; i++) res = lcm(res, i);
int up = 0;
for (int i = 1; i <= n; i++) up += res / i;
up *= n;
int g = __gcd(up, res);
up /= g, res /= g;
int t = up / res, t1 = up % res;
if (t1 == 0) cout << t << endl;
else {
// t1 /= __gcd(res, t1), res /= __gcd(res, t1);
for (int i = 1; i <= get(t) + 1; i++) cout << ' ';
cout << t1 << endl;
cout << t << ' ';
for (int i = 1; i <= max(get(t1), get(res)); i++) cout << '-';
puts("");
for (int i = 1; i <= get(t) + 1; i++) cout << ' ';
cout << res << endl;
}
}
}
「概率 DP」
例题 7: Bag of mice CF148D
题目大意:袋子里有 \(w\) 只白鼠和 \(b\) 只黑鼠 (\(1\leq w,b \leq 1000\)),A 和 B 轮流从袋子里抓,谁先抓到白色谁就赢。A 每次随机抓一只,B 每次随机抓完一只之后会有另一只随机老鼠跑出来。如果两个人都没有抓到白色则 B 赢。A 先抓,问 A 赢的概率。
思路
考虑到 \(w,b\) 只有 \(1000\),那我们可以开一个数组 \(dp_{i,j}\) 表示轮到 A 抓老鼠时面对 \(i\) 个白鼠和 \(j\) 个黑鼠获胜的概率。
分三种情况讨论:
-
若 A 这一次直接抓到白鼠,\(dp_{i,j}=\dfrac{i}{i+j}\)。
-
若 A 这一次抓到了黑鼠,因为 A 要赢,所以下次 B 也抓的是黑鼠。如果下次 B 抓的时候跑的是白鼠,则 \(dp_{i,j}=\dfrac{j}{i+j}\times \dfrac{j-1}{i+j-1}\times \dfrac{i}{i+j-2}\times dp_{i-1,j-2}\)。
-
若 A 这一次抓到了黑鼠,因为 A 要赢,所以下次 B 也抓的是黑鼠。如果下次 B 抓的时候跑的是黑鼠,则 \(dp_{i,j}=\dfrac{j}{i+j}\times \dfrac{j-1}{i+j-1}\times \dfrac{j-2}{i+j-2}\times dp_{i,j-3}\)。
初始化为:\(dp_{i,0}=1,dp_{0,i}=0\)
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int w, b;
double dp[N][N];
int main() {
cin >> w >> b;
for (int i = 1; i <= w; i++) dp[i][0] = 1.0; // dp_{0,i} 可以不初始化,因为dp均为0
for (int i = 1; i <= w; i++) {
for (int j = 1; j <= b; j++) {
dp[i][j] = i * 1.0 / (i + j);
if (j >= 2) dp[i][j] += 1.0 * j / (i + j) * 1.0 * (j - 1) / (i + j - 1) * 1.0 * i / (i + j - 2) * dp[i - 1][j - 2];
if (j >= 3) dp[i][j] += 1.0 * j / (i + j) * 1.0 * (j - 1) / (i + j - 1) * 1.0 * (j - 2) / (i + j - 2) * dp[i][j - 3];
}
}
printf("%.9lf", dp[w][b]);
}
例题 8:POJ3744 Scout YYF I
题目大意:在一条有地雷的路上,你现在的起点在 \(1\) 处(保证 \(1\) 没雷)。在 \(N\) 个点处布有地雷。每次有 \(p\) 的概率前进一步,\(1-p\) 的概率前进 \(2\) 步。问顺利通过这条路的概率。(不要走到有地雷的地方)。 \(1\leq N\leq 10\),地雷点的坐标范围:\([1,100000000]\)。
思路
先考虑普通概率 dp,定义 \(dp_i\) 表示到第 \(i\) 点能活下来的概率。
若第 \(i\) 个点有雷,不管咋样 \(dp_i=0\)。
否则 \(dp_{i+1}=dp_i\times p,dp_{i+2}=dp_i\times (1-p)\)。
初始化 \(dp_1=1\)。
然后你写到一半发现是错的,因为地雷点坐标范围实在太大了,数组存不下。
于是你考虑缩小距离,若两个地雷点之间的距离大于 \(2000\),直接将其强制设为 \(2000\)。(当然后面的地雷也要进行整体前移)
由于最多 \(10\) 个地雷,所以数组只需要开 \(10\times 2000=20000\) 就够了。
正确性很正确,因为如果连续都没有地雷,那么误差实在太小了,可以忽略不计。距离如果设置得过小也过不去。正解其实是矩阵优化 dp。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 20005;
int n, a[N], vis[N];
double dp[N];
double p;
int main() {
while (cin >> n >> p) {
memset(dp, 0, sizeof dp);
memset(vis, 0, sizeof vis);
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + n + 1);
for (int i = 0; i < n; i++) {
if (a[i + 1] - a[i] > 2000) {
int dis = a[i + 1] - a[i];
for (int j = i + 1; j <= n; j++) a[j] -= (dis - 2000);
}
}
if (a[1] == 1) {
printf("%.7lf", 0);
continue;
}
for (int i = 1; i <= n; i++) vis[a[i]] = 1;
dp[1] = 1.0;
for (int i = 1; i <= a[n]; i++) {
if (vis[i])
dp[i] = 0;
else {
dp[i + 1] += dp[i] * p;
dp[i + 2] += dp[i] * (1.0 - p);
}
}
printf("%.7lf\n", dp[a[n] + 1]);
}
}
「综合题目」
P3251 [JLOI2012] 时间流逝
形式化题意
给定一长为 \(N\) 的有序序列 \(a\),序列内元素两两不同。你现在有一个栈,初始为空,你会不断执行以下操作直到栈内元素之和 \(>T\)。一次「操作」过程如下:
-
如果栈不为空,则有 \(P\) 的概率弹出一个元素。
-
如果没弹,在所有 \(\le\) 栈顶的 \(a_i\) 里随机地取一个,将其入栈(不把 \(a_i\) 删掉,下一次也可能继续入栈)。
问操作终止时进行操作次数的期望。
思路
可以发现,栈底到栈顶的数单调递减的。尝试在相邻的状态连边,从上一种状态到下一种可到达的状态连有向边。发现每一个状态删去末尾的数后,对应唯一的状态,所以所有的状态连完边后形成一个树形结构。其中,叶子节点就是元素和 \(>T\) 的点。
定义 \(f_x\) 表示编号 \(x\) 状态走到叶子结点的期望步数,对于一个点,有 \(P\) 概率走到父亲,有 \(\frac{1-P}{son_x}\) 概率走到这 \(son_x\) 个儿子,所以有如下转移:
化简后得到:$$f_x=1+f_{fa}\times P+\sum f_{son}\times \frac{1-P}{son_x}$$
这个表示非常麻烦,但是有个东西叫做“树上高斯消元”,直接待定系数,设 \(f_x=k_x\times f_{fa}+k_b\)。
化简得:
所以:
一边搜索一边转移 \(k,b\) 即可。

浙公网安备 33010602011771号