「算法学习」概率与期望

「离散概率初步」

连续抛 \(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=(f_{fa}+1)\times P+\sum (f_{son}+1)\times \frac{1-P}{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\)

化简得:

\[f_x = 1 + P \times f_{fa} + \frac{1 - P}{cnt_{x}} \times \sum_{y \in son} (k_{y} \times f_x + b_y) \]

\[f_x = \frac{1 - \frac{P}{cnt_{x}} \times \sum_{y \in son} k_{y}}{1 - \frac{1 - P}{cnt_{x}} \times \sum_{y \in son} k_{y}} \times f_{fa} + \frac{1 + \frac{1 - P}{cnt_{x}} \sum_{y \in son} b_y}{1 - \frac{1 - P}{cnt_{x}} \sum_{y \in son} k_{y}} \]

所以:

\[k_x = \frac{1 - \frac{P}{cnt_{x}} \times \sum_{y \in son} k_{y}}{1 - \frac{1 - P}{cnt_{x}} \times \sum_{y \in son} k_{y}} \]

\[b_x = \frac{1 + \frac{1 - P}{cnt_{x}} \sum_{y \in son} b_y}{1 - \frac{1 - P}{cnt_{x}} \sum_{y \in son} k_{y}} \]

一边搜索一边转移 \(k,b\) 即可。

posted @ 2024-12-26 21:44  Otue  阅读(60)  评论(0)    收藏  举报