概率&期望
好久没写博客了,最近刷完了概率期望的专题,特地总结一下。
概率
(1) 基本概率知识
学过概率论课程的话,下面的都是基础了。
①条件概率: p(A|B)=p(AB)p(B) p ( A | B ) = p ( A B ) p ( B ) 。
P(A|B) P ( A | B ) 指B发生的条件下A发生的概率。
②全概率: p(A)=∑ni=1p(A|Bi) p ( A ) = ∑ i = 1 n p ( A | B i ) 。
全概率公式的关键在于划分样本空间,需要把所有可能情况不重复、不遗漏地进行分类,并计算出每个分类下事件A发生的概率。
③贝叶斯公式: p(Bi|A)=p(Bi)∗p(A|Bi)∑ni=1p(A|Bi) p ( B i | A ) = p ( B i ) ∗ p ( A | B i ) ∑ i = 1 n p ( A | B i ) 。
④ 古典概型和几何概型:古典概型是最基本的概率计算模型,适用于有限个随机事件的情况,而几何概型则适用于无限个随机事件的情况,几何概型的基本思想是把事件与几何区域对应,利用几何区域的度量来计算事件发生的概率。
(2) 练习
概率问题是一类很常见的问题,大部分都很容易求解。
一般的概率题都只分两种,概率dp,以及普通的概率计算。普通的概率计算,就是应用的基本的概率知识了,不得不说概率论的内容还是挺重要的。概率dp只是状态转移时乘上相应的概率罢了。
uva11722 Joining with Friend
传送门
题意:两人会面,第一个人去的时间在[t1, t2]中,第二个人去的时间在[s1, s2]中,两人会面成功的话,到达会面地点的时间差不能大于w,求两人成功会面的概率。
分析:经典的几何概型,想必数学书上概率部分都有介绍此问题。
几何概型最大的一个特点是通过给定的条件可以将概率分布归于一个几何区域,而所有可能的分布情况也是一个几何区域。那么可以通过几何区域的度量来完成概率计算。常见的一般都是二维随机变量的分布。
本题中情况比较多,考虑需要仔细,计算会面成功的面积可以通过反面来考虑,把不符合的面积减去,每一种概率都不同。
#include <bits/stdc++.h>
using namespace std;
bool cal(int m, int l, int r) {
return l <= m && m <= r;
}
double l, h, t1, t2, s1, s2, tota;
double solve(double w) {
double ux, dx, mly, mry;
mly = t1 - w; mry = t2 - w;
ux = s2 + w; dx = s1 + w;
double are;
if (cal(mly, s1, s2) && cal(ux, t1, t2)) {
are = (s2 - mly) * (ux - t1) / 2.0;
return w < 0 ? are : tota - are;
}
else if (cal(mly, s1, s2) && cal(mry, s1, s2)) {
are = (2 * s2 - mly - mry) * l / 2.0;
return w < 0 ? are : tota - are;
}
else if (cal(dx, t1, t2) && cal(mry, s1, s2)) {
are = (t2 - dx) * (mry - s1) / 2.0;
return w < 0 ? tota - are : are;
}
else if (cal(dx, t1, t2) && cal(ux, t1, t2)) {
are = (ux + dx - 2 * t1) * h / 2.0;
return w < 0 ? are : tota - are;
}
if (w < 0 && mry <= s1 && dx >= t2) return tota;
if (w > 0 && mly >= s2 && ux <= t1) return tota;
return 0;
}
int main() {
int t, ca = 0;
scanf("%d", &t);
while (t--) {
double w;
scanf("%lf %lf %lf %lf %lf", &t1, &t2, &s1, &s2, &w);
l = t2 - t1; h = s2 - s1;
tota = l * h;
double ar = tota - solve(w) - solve(-w);
printf("Case #%d: %.8f\n", ++ca, ar / tota);
}
return 0;
}
xtu 1244 Gambling
传送门
题意:袋子中有a个红球,b个绿球,c个蓝球,不放回地取球,如果某一种球取完了,则游戏结束,先取完红球得一等奖,先取完绿球得二等奖,先取完蓝球得三等奖。求得一等、二等、三等奖的概率。
分析:这题一开始是直接概率dp的。。。然而根本不可做,因为abc范围太大,都达到了1e3,且题目输出的是一个分子/分母的形式,交上去都直接t掉了。。。
要解决还是需要一定的数学基础的。考虑先取完红球的情况,所有可能情况无外乎两种:
(1) 先取完红,在取完绿球,在取完蓝球;
蓝球是最后一个取完的概率是 ca+b+c c a + b + c ,这个概率论课上讲过一个结论,第n次取得蓝球的概率和第一次取得蓝球的概率是相同的,这个可以通过多重集排列证明。
还需要保证绿球晚于红球取完,而绿球在红球和绿球的排列中是最后一个的概率是 ba+b b a + b ,很容易知道这两者的交集包含了所有的先取完红,在取完绿球,在取完蓝球的情况,所以第一种情况的概率就是两者相乘。
(2) 先取完红,在取完绿球,在取完蓝球;
与上面类似,概率为 ba+b+c∗ca+c b a + b + c ∗ c a + c 。
那么先取完红球的概率就是 bc(a+b+c+a)(a+b+c)∗(a+c)∗(a+b) b c ( a + b + c + a ) ( a + b + c ) ∗ ( a + c ) ∗ ( a + b ) 。
先取完绿球和先取完蓝球的概率同理可得分别为 ac(a+b+c+b)(a+b+c)∗(b+c)∗(a+b),ab(a+b+c+c)(a+b+c)∗(b+c)∗(a+c) a c ( a + b + c + b ) ( a + b + c ) ∗ ( b + c ) ∗ ( a + b ) , a b ( a + b + c + c ) ( a + b + c ) ∗ ( b + c ) ∗ ( a + c ) 。
从本题可以看出,在一些概率的计算问题中只要选取合适的分类,那么计算也会相应变得清楚和简单了。培养自己多角度思考问题和观察问题的能力还是很重要的。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int main() {
LL a, b, c;
while (~scanf("%I64d %I64d %I64d", &a, &b, &c)) {
LL sum = a + b + c;
LL fraa = b * c * (sum + a), numa = sum * (a + b) * (a + c);
LL frab = a * c * (sum + b), numb = sum * (a + b) * (b + c);
LL frac = b * a * (sum + c), numc = sum * (c + b) * (a + c);
LL ga = __gcd(fraa, numa), gb = __gcd(frab, numb), gc = __gcd(frac, numc);
printf("%I64d/%I64d %I64d/%I64d %I64d/%I64d\n", fraa / ga, numa / ga, frab / gb, numb / gb, frac / gc, numc / gc);
}
return 0;
}
csu 1917 There is no SSR
传送门
题意:阴阳师抽奖,得到R的概率为p,得到SR的概率为1-p,求抽m次奖连续获得n个SR的概率。
分析:多校时这题题意都没看懂。。。in succession是连续的意思,本蒟蒻一眼看上去以为是成功得到n个SR的概率。。。。。
可以反过来考虑,求出不会连续得到n个SR的概率,这个利用dp便可以很简单的求出了,令dp[i]为抽i次奖不会连续得到n个SR的概率,
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
struct mat{
int n, m;
double a[N][N];
mat(int _n, int _m) : n(_n), m(_m) {}
mat() {}
void init() {
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++) a[i][j] = 0;
}
}a;
double po[N];
mat mul(mat a, mat b) {
mat c(a.n, b.m); c.init();
for (int i = 0; i < c.n; i++) {
for (int j = 0; j < c.m; j++) {
for (int k = 0; k < a.m; k++) {
c.a[i][j] += a.a[i][k] * b.a[k][j];
}
}
}
return c;
}
double matpow(int n) {
mat b(a.n, 1); b.init();
for (int i = 0; i < a.n; i++) b.a[i][0] = 1;
while (n) {
if (n & 1) b = mul(a, b);
a = mul(a, a);
n >>= 1;
}
return b.a[0][0];
}
int main() {
double p, q;
int n, m;
while (~scanf("%lf %lf %d %d", &p, &q, &n, &m)) {
if (q == 0) { puts("0.000000"); continue; }
po[0] = 1; a = mat(n, n); a.init();
for (int i = 1; i <= n; i++) po[i] = po[i-1] * q;
for (int i = n - 1; ~i; i--) a.a[0][i] = po[i] * p;
for (int i = 1; i < n; i++) a.a[i][i-1] = 1.0;
printf("%f\n", 1.0 - matpow(m - n + 1));
}
return 0;
}
uvalive6672 Bonus Cards
传送门
题意:有n张票,有a个拥有icpc card的人,b个拥有acm card的人,每次会进行一轮抽奖,拥有icpc card的人抽奖时获得2个slot,拥有acm card的人抽奖时获得1个slot,然后随机选择一个slot,那么这个slot的拥有者得到票,得到票的人下次不会参与抽奖,抽奖的过程在n轮之后或者每个人都得到票之后结束。现在还有一个人,他想知道自己在拥有一张icpc card和一张acm card这两种情况下与这a+b个人来抢票时成功得到票的概率。
分析: 这题是明显的概率dp,在额外一个拥有icpc card的人参与抽奖的情况下,令dpi[i][j]为前i轮有j个拿icpc card的人得到了票的概率。在额外一个拥有acm card的人参与抽奖的情况下,令dpa[i][j]为前i轮有j个拿icpc card的人得到了票的概率。
dp时采用刷表的方式比较方便,这题其实不难,但是比较卡时间。。。。动不动就t了。
考虑几个优化:
①如果dp[i][j]接近0,那么可以不用转移了,因为乘上概率之后更小了,加上去对于1e-9的精度无影响。
②计算时采用乘法比除法更快。
③初始化时只清零能用到的值。
第一个优化节省了接近4000ms的时间。。。
本题的优化方式可以说是非常常见和重要的的,同样地可以利用滚动数组实现空间上面的优化。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3001;
double dpi[2][N], dpa[2][N];
int main() {
int n, a, b;
while (~scanf("%d %d %d", &n, &a, &b)) {
if (n > a + b) { puts("1.0\n1.0"); continue; }
double resi = 0, resa = 0;
dpa[0][0] = 1.0; dpi[0][0] = 1.0;
bool f = 1;
for (int i = 0; i < n; i++, f = !f) {
int up = min(i, a) + 1;
memset(dpa[f], 0, 8LL * (up + 1));
memset(dpi[f], 0, 8LL * (up + 1));
for (int j = 0; j < up; j++) {
if (abs(dpi[!f][j]) < 1e-8) continue;
double sa = (a - j) << 1, sb = b - i + j, ti = dpi[!f][j], ta = dpa[!f][j];
double s = 1.0 / (2.0 + sa + sb), s1 = 1.0 / (1.0 + sa + sb);
if (sa) {
dpi[f][j+1] += ti * sa * s;
dpa[f][j+1] += ta * sa * s1;
}
if (sb) {
dpi[f][j] += ti * sb * s;
dpa[f][j] += ta * sb * s1;
}
resi += ti * 2.0 * s;
resa += ta * s1;
}
}
printf("%.10f\n%.10f\n", resi, resa);
}
return 0;
}
hdu 5985 Lucky Coins
传送门
题意:有k(k<=10)种硬币,每种硬币的数量和其抛一次正面向上的概率已知。会进行如下的过程:将每枚硬币向上抛一次,每次把正面朝下的硬币给去掉。重复上面的过程直到某一次只剩下了一种硬币或者没有硬币有剩余为止。如果结束时还剩下一种硬币,那么该种硬币即为幸运硬币,现在需要求每一种硬币成为幸运硬币的概率。
分析:一看上去就觉得可做,但是情况比较多,本蒟蒻刚开始也陷在里面了。。
首先这里给出了硬币正面向上的的概率,以及6位精度,我们只需要枚举每枚硬币在i轮死亡,其他硬币在第i轮之前死亡的的情况即可,可以知道i枚举到100也已经more than enough了~所以可以直接dp出每种硬币在第j轮死亡的概率,那么我们然后统计下dp数组的前缀和,然后枚举轮数即可。但是似乎复杂度比较高。
从反面考虑这个问题,不妨这样计算:
令 f[i][j] f [ i ] [ j ] 表示第i枚硬币在前 j j 轮之前死亡的概率。这样
,这里 pi p i 是第i种硬币正面向上的概率, numi n u m i 是其数量。
令 g[i][j] g [ i ] [ j ] 表示第i种硬币在j轮之后仍然存活的概率,那么 g[i][j]=1−f[i][j] g [ i ] [ j ] = 1 − f [ i ] [ j ] 。
为了不重复统计,我们枚举第i种硬币死亡的轮数。
那么
#include <bits/stdc++.h>
using namespace std;
const int N = 12, M = 110;
int num[N];
double p[N], d[N][M], a[N][M], ans[N];
double qpow(double x, int n) {
double res = 1.0;
while (n) {
if (n & 1) res *= x;
x *= x; n >>= 1;
}
return res;
}
int main() {
int t;
scanf("%d", &t);
while (t--) {
int k;
scanf("%d", &k);
for (int i = 0; i < k; i++) scanf("%d %lf", num + i, p + i);
if (k == 1) { puts("1.000000"); continue; }
for (int i = 0; i < k; i++) {
double tp = p[i];
for (int j = 1; j <= 100; j++, tp *= p[i]) d[i][j] = qpow(1.0 - tp, num[i]), a[i][j] = 1.0 - d[i][j];
}
for (int i = 0; i < k; i++) {
ans[i] = 0;
for (int j = 1; j <= 100; j++) {
double tm = 1.0;
for (int x = 0; x < k; x++) if (x != i) tm *= d[x][j];
ans[i] += (a[i][j] - a[i][j+1]) * tm;
}
}
for (int i = 0; i < k; i++) printf("%f%c", ans[i], i == k - 1 ? '\n' : ' ');
}
return 0;
}
hdu 5955 Guessing the Dice Roll
传送门
题意:有n个长为L的1~6的序列,一直投骰子,并把骰子的数记录下来,当记录下来的序列匹配某一个序列时,游戏结束,求游戏结束后每个序列被匹配的概率。
分析:这里是明显的概率dp,不过注意到这里是匹配问题,我们可以用ac自动机构造当前的状态并向下一个状态转移。在得到方程组之后就可以直接高斯消元了~
//精度要求比较高,消元时的eps要设置小一点,比赛时没注意到这个问题然后一直wa,然后被队友嫌弃…后面把eps改为1e-14就过了。
#include <bits/stdc++.h>
using namespace std;
const int maxnode = 222, sigma_size = 7;
struct AC_Automata {
int ch[maxnode][sigma_size];
int val[maxnode];
int f[maxnode];
int sz;
void init() {
sz = 1;
memset(ch[0], 0, sizeof(ch[0]));
val[0] = 0;
}
int Insert(int *s, int n) {
int u = 0;
for (int i = 0; i < n; i++) {
int id = s[i] - 1;
if (ch[u][id] == 0) {
ch[u][id] = sz;
memset(ch[sz], 0, sizeof(ch[sz]));
val[sz++] = 0;
}
u = ch[u][id];
}
val[u] = 1;
return u;
}
void getFail() {
queue<int> q;
f[0] = 0;
for (int i = 0; i < sigma_size; i++) {
int u = ch[0][i];
if (u) {
f[u] = 0;
q.push(u);
}
}
while (!q.empty()) {
int r = q.front();
q.pop();
for (int i = 0; i < sigma_size; i++) {
int u = ch[r][i];
if (u == 0) {
ch[r][i] = ch[f[r]][i];
continue;
}
q.push(u);
int v = f[r];
f[u] = ch[v][i];
val[u] |= val[f[u]];
}
}
}
};
AC_Automata ac;
vector<int> g[maxnode];
int pos[12], s[12];
struct GAUSS_double {
static const int MAXN = 210;
const double EPS = 1e-14;
double a[MAXN][MAXN];
double x[MAXN];
bool free_x[MAXN];
int gauss(int equ, int var) {
int i, j, k, max_r, col, free_index, free_num;
memset(free_x, 1, sizeof(free_x));
memset(x, 0, sizeof(x));
for (k = col = 0; k < equ && col < var; ++k, ++col) {
max_r = k;
for (int i = k + 1; i < equ; ++i) {
if (abs(a[i][col]) - abs(a[max_r][col]) > EPS) {
max_r = i;
}
}
if (max_r != k) for (int j = k; j <= var; ++j) swap(a[max_r][j], a[k][j]);
if (abs(a[k][col]) <= EPS) { --k; continue; }
for (int i = k + 1; i < equ; ++i) {
if (abs(a[i][col]) <= EPS) continue;
double tmp = a[i][col] / a[k][col];
for (int j = col; j <= var; ++j) {
a[i][j] -= a[k][j] * tmp;
}
}
}
for (int i = var - 1; i >= 0; --i) {
double tmp = a[i][var];
for (int j = i + 1; j < var; ++j) {
if (abs(a[i][j]) > EPS) tmp -= x[j] * a[i][j];
}
x[i] = tmp / a[i][i];
}
return 0;
}
} G;
void init(int n) {
double x = 1.0 / 6;
for (int i = 0; i <= ac.sz + 2; i++)
for (int j = 0; j < ac.sz + 5; j++) G.a[i][j] = 0;
for (int i = 1; i <= ac.sz; i++) {
G.a[i][i] = 1.0;
for (int j : g[i]) G.a[i][j] -= x;
}
for (int i = 0; i < n; i++) G.a[ac.sz + 1][pos[i]] = 1.0;
G.a[ac.sz + 1][ac.sz + 1] = 1.0;
}
int main() {
int ca;
scanf("%d", &ca);
while (ca--) {
int n, L;
scanf("%d%d", &n, &L);
ac.init();
for (int i = 0; i < n; i++) {
for (int j = 0; j < L; j++) scanf("%d", &s[j]);
pos[i] = ac.Insert(s, L);
}
ac.getFail();
for (int i = 0; i < maxnode; i++) g[i].clear();
for (int i = 0; i < ac.sz; i++) {
if (ac.val[i]) continue;
for (int j = 0; j < 6; j++) {
int u = ac.ch[i][j];
if (u) g[u].push_back(i);
else g[ac.sz].push_back(i);
if (u != 0 && i == 0) g[u].push_back(ac.sz);
}
}
init(n);
G.gauss(ac.sz + 2, ac.sz + 1);
for (int i = 0; i < n; i++) printf("%f%c", G.x[pos[i]], i == n - 1 ? '\n' : ' ');
}
return 0;
}
CFGym 100608G Greater Number Wins
传送门
题意:两个玩家玩游戏,每个玩家有一行总共d个格子,会玩两个版本的游戏,第一个游戏中,两个玩家轮流操作,每次操作该玩家随机得到一个0~b-1之间的随机数,然后把这个数填在他自己的一个未被填的格子上,
如果最后两个玩家都填满了d个格子,那么比较两个玩家所得到的d位b进制数,数字大的玩家赢,第二个游戏中,第一个玩家先操作d次,然后第二个玩家操作d次。
求两个游戏中无论第二个玩家如何操作,第一个玩家都能赢的最大概率。
分析:
第一次做asc的题。。果然我这种智商为0的人想的特别简单。。。认为一定把数字大的优先放在高位,这里存在一个明显的问题,游戏都没结束,怎么知道当前抽的这个数字是大还是小。。。
其实是根据样例直接猜测的。。然后第二个样例就wa了。
这样我们自然而然的考虑到了dp,令dp[i][j]表示当前第一个玩家填的数字是i,第二个玩家当前填的数字是j的情况下第一个玩家能赢的最大概率。
这样,我们直接记忆化dp,暴力枚举当前状态下每个人得到的数字以及填的位置,第一个玩家会最大化概率,第二个玩家当然会最小化概率。
这里状态中需要考虑当前位置是否已经填过,我们只需要把数字+1就行了,如果当前位大于0,那么必定是已经填过的。
两种游戏转移过程有点区别而已。
不过快要超时了。。可以把所有可能的情况打个表,情况不过几十种。
#include <bits/stdc++.h>
using namespace std;
const int N = 3001;
bool vis[N][N];
double dp[N][N];
int p[12][12], b, d, tb;
void init() {
for (int i = 2; i <= 11; i++) {
p[i][0] = 1;
for (int j = 1; j < 10; j++) p[i][j] = p[i][j-1] * i;
}
}
double dfs1(int x, int y) {
if (vis[x][y]) return dp[x][y];
vis[x][y] = 1;
int sx[12], sy[12], c = 0, tx = x, ty = y;
for (int i = 0; i < d; i++, tx /= tb, ty /= tb) {
sx[i] = tx ? tx % tb : 0;
sy[i] = ty ? ty % tb : 0;
if (sx[i]) c++;
}
if (c == d) return dp[x][y] = x > y ? 1.0 : 0;
double res = 0;
for (int i = 1; i <= b; i++) {
double ma = -1.0;
for (int j = 0; j < d; j++) if (!sx[j]) {
double sum = 0;
int nx = x + p[tb][j] * i;
for (int _i = 1; _i <= b; _i++) {
double mi = 2.0;
for (int _j = 0; _j < d; _j++) if (!sy[_j]) {
mi = min(mi, dfs1(nx, y + p[tb][_j] * _i));
}
sum += mi;
}
ma = max(ma, sum / b);
}
res += ma;
}
return dp[x][y] = res / b;
}
double dfs2(int x, int y) {
if (vis[x][y]) return dp[x][y];
vis[x][y] = 1;
int sx[12], cx = 0, tx = x;
for (int i = 0; i < d; i++, tx /= tb) {
sx[i] = tx ? tx % tb : 0;
if (sx[i]) cx++;
}
if (cx == d) {
int sy[12], cy = 0, ty = y;
for (int i = 0; i < d; i++, ty /= tb) {
sy[i] = ty ? ty % tb : 0;
if (sy[i]) cy++;
}
if (cy == d) return dp[x][y] = x > y ? 1.0 : 0;
else {
double res = 0;
for (int i = 1; i <= b; i++) {
double mi = 1.0;
for (int j = 0; j < d; j++) if (!sy[j]) {
mi = min(mi, dfs2(x, y + p[tb][j] * i));
}
res += mi;
}
return dp[x][y] = res / b;
}
}
else {
double res = 0;
for (int i = 1; i <= b; i++) {
double ma = -1.0;
for (int j = 0; j < d; j++) if (!sx[j]) {
ma = max(ma, dfs2(x + p[tb][j] * i, y));
}
res += ma;
}
return dp[x][y] = res / b;
}
}
int main() {
freopen("greater.in", "r", stdin);
freopen("greater.out", "w", stdout);
init();
int fi = 0;
while (scanf("%d %d", &d, &b) && b + d) {
tb = b + 1;
if (fi) memset(vis, 0, sizeof(vis));
else fi = 1;
printf("%.10f ", dfs1(0, 0));
memset(vis, 0, sizeof(vis));
printf("%.10f\n", dfs2(0, 0));
}
return 0;
}
hdu 5819 Knights
传送门
题意:数轴上有n个骑士位于1,2,3,…n,移动速度相同,初始移动方向已知,当两个骑士相遇时,各有50%的概率赢,输了就死了,并且移动到0和n+1的位置时移动方向会翻转,问最右的骑士存活的概率。
分析:可以发现这个问题中速度和时间都是无关紧要的,第 n n 个骑士如果要赢,那么必然是他向左能打败前面所有存活下来的向右的骑士。
考虑
表示前 i i 个骑士有
个是向右走的概率。第 i i 个骑士如果向左,那么有转移
,枚举上一阶段有 k k 多少个骑士向右走,那么第
个骑士打败了 k−j k − j 个骑士之后死亡。这里我们可以看出来 dp[i][j]=dp[i][j+1]+dp[i−1][j]2 d p [ i ] [ j ] = d p [ i ] [ j + 1 ] + d p [ i − 1 ] [ j ] 2 。
如果第i个骑士向右,那么 dp[i][j]=dp[i−