概率和期望

概率和期望

期望

常见技巧与知识

  • 如果当前步数通往下一步时,有 p 的概率原地打转,则走到下一步的期望步数为 11p
  • 如果在进行某个操作时达到要求则停止,求期望步数,则可设达到要求后不停止,但不耗步数,保持问题的对称性
  • 求步数的期望:ans=E[步数]=i=0Pr[i步后不满足要求]
  • 套路设计状态:设 f(i) 为已经走了 i 步时,走完的期望步数

一些公式:

条件期望:如果 A 不发生时,随机变量 X 的值取 0,那么 E=E(X|A)P(A)

全期望公式:如果一组事件 C1n 满足它们之间恰好有一个会发生,即 i=1nP(Ci)=1,那么

E(A)=i=1nE(A|Ci)P(Ci)

这在 DP 中很常见

特例:E(A)=E(A|B)P(B)+E(A|B¯)P(B¯)
如果两个变量 X,Y 独立,即其结果互不影响,P((X=a)(Y=b))=P(X=a)P(Y=b),那么 E(XY)=E(X)E(Y)

这个时候期望才能乘

注意:X 只有是常数时才和自己独立,独立性不传递,即 X,YY,Z 分别互相独立,但 X,Z 不一定互相独立


期望逆推

现在我还不是很确定为什么期望倒推,有两种说法我觉得都有道理:一是这种期望题末状态未知,但初状态已定,有可能某些末状态根本到不了,顺推的话还要计算到达它的概率,逆推比较方便;二是有人说倒推是为了保证转移时各种情况的概率之和为 1,保证正确性。可能两种原因都有?

T1:P4550 收集邮票

因为邮票的花费与次数相关——要什么转什么,还要转期望次数

套路设计状态:f[i]已经买到i张邮票,买全n张的期望次数

分2种情况讨论:

  1. 下次不幸买到已有的i张,概率为 in,还要再用 f[i] 的次数买

  2. 幸运的买了没有的 ni 张,概率为 nin,用 f[i+1] 的次数即可

f[i]=in×f[i]+nin×f[i+1]+1 (本次花了 1 次买,+1)

那钱数呢?买 k 次总钱数为 (k+1)×k2,但期望不能这样算

套路设计状态:g[i] 为已经买到 i 张邮票,买全 n 张的期望总钱数

但我们不清楚已经买 i 张的总次数,只知道后面的次数 f[i]

那就把题改一下:购买倒数第 k 次邮票需要 k(反过来先加后加一样)

还是分情况:

  1. 下次不幸买到已有的 i 张,概率为 in,本次花费 f[i]+1 元(注意本次以后还有 f[i]+1),以后要 g[i]

  2. 幸运的买了没有的 ni 张,概率为 nin,本次花费 f[i+1]+1 元(本次以后还有 f[i+1]),以后要 g[i+1]

g[i]=in×(g[i]+f[i]+1)+nin×(g[i+1]+f[i+1]+1)

两边都有 g[i],化简一下:

g[i]=ini×f[i]+f[i+1]+g[i+1]+nni

易知 g[n]=f[n]=0,刚开始没邮票,求到 g[0] 即可

代码:

#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;
}

T2:P3750 [六省联考 2017] 分手是祝愿

又一道期望神题

这里只用求次数,状态很神奇:

f[i] 为从i 个灯要按到有 i1 个灯要按的期望次数

推转移方程:

f[i]={i,i<=kin+nin×(f[i+1]+f[i]+1),i>k

解释第二种情况:现在有 in 的概率按到要按的,一次就按到 i1,还有 nin 的概率按到不用按的,先从 i+1 个按到第 i 个,再从第 i 个按到 i1 个(注意一开始按了一个,加 1

第一个很好求,最后加 k 即可

化简第2个:

in×f[i]=in+nin×f[i+1]+nin

f[i]=ni×(nin×f[i+1]+1)=(ni)×f[i+1]+ni

注意一下状态结尾不是 f[n],因为我们总共不一定要 n

O(nn) 求出按几次就好了(注意特判可能总共只要 k 次及以下)

代码:

#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;
}

T3:P4206 [NOI2005] 聪聪与可可

f(i,j) 表示聪聪在 i,可可在 j 时,聪聪抓到可可的期望步数

先预处理出图中点两两之间的距离

如果 i=j,显然 f(i,j)=0

让聪聪先靠近可可 2 步,如果抓到了,f(i,j)=1

否则 f(i,j)=k=j||(j,k)E1degj+1f(pos,k)

看起来要高斯消元,但复杂度不允许

仔细分析一下,由于聪聪一定会靠近可可,所以可可在 k,聪聪在 pos 时,聪聪不可能回到 i,因为这样就远离了可可

发现转移之间无环,可以直接记忆化搜索,或是建出图拓扑排序

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 方法

T4:P2473 [SCOI2008] 奖励关

n 的范围显然得考虑状压 DP,设 f(i,s) 为已经抛了 i 个礼物,获得礼物的集合为 s 时剩下得分的最大期望

枚举这一轮的礼物种类,如果 s 中包含了所有的前置礼物,则决策可以时选它,f(i+1,s|2j)+wjf(i,s)

否则不能选,f(i+1,s)f(i,s)

稍微进行剪枝,预处理出合法的集合 s

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;
}

期望线性

期望的线性性很重要!

E(aX+bY)=E(aX)+E(bY)=aE(X)+bE(Y)

但是乘积的期望不那么好处理,尤其是相关的时候

通常是大力拆括号/组合意义/结合各种优化来 DP

还有将问题等价去掉限制,有的时候要大胆猜测

CF1842G Tenzing and Random Operations

计算 i=1n(ai+kiv) 的期望

拆开的话很复杂,ki 相乘的期望也不好算

但我们知道展开后一定是若干个 n 个数乘积的单项式相加

于是算出所有可能的选择方案中这些单项式的总和即可知道期望

考虑有这样的数表:

[x1,1x1,2x1,n1x1,nx2,1x2,2x2,n1x2,nxm,1xm,2xm,n1xm,n]

xi,j 表示第 i 次操作,第 j 个位置是否被增加 v

每个位置的值为 0/1,且一行中为 1 的一定是一个后缀(因为是后缀加)

操作次数很多,但是这个单项式只有 n 个数

每个位置的 +v 有能不能用,有没有用过两种状态

我们不关心它能不能用,我们只关心它用了没有,而且总共只会用 O(n)

也就是说,没有用到的操作我们不关心它具体的起点,将它们归为一类计算贡献

这样设计 f(i,j) 表示已经统计了前 i 个数,用了 j 次操作的总和

  • i 个数放 aif(i,j)f(i1,j)×ai
  • i 个数想选之前已用过的操作,此时根据 x 的性质,x 肯定是 1,能用上,有 j 种选法,f(i,j)f(i1,j)×j×v
  • i 个数想选之前没用过的操作,必须把这个操作在前面某次就钦定为 1,有 mj+1 个操作,前面有 i 个可用起点可供选择,f(i,j)f(i1,j1)×(mj+1)×i×v

最后统计答案,用了 i 次操作时,剩下 mi 次操作可以随便指定起点,共 nmi 种方法

期望还要除以 nm,化简后 ans=i=0min{n,m}f(n,i)ni

或者直接 DP 期望,E(i,j) 为已经统计了前 i 个数,用了 j 次操作的某种摆放方案中单项式的总和的期望,放 ai 期望直接乘 ai,放之前用过的,由于 x=1 所以一定能成功,期望 ×j×v,放没用过的,它有 in 的概率能成功,否则 x=0,整个式子值直接为 0,所以 E(i,j)E(i1,j1)×in×(mj+1)×v

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;
}

ARC150D Removing Gacha

首先转化 E(使所有点被选中的期望次数)=i=1nE(i 被选中的次数),因为只有所有点被选中后才没有点被选,而期望有线性性,可以拆开

这样计算一个点被选中的次数,只跟这个点到根的这条链上的点有关

x 只有当链上所有点被选过后才不会被选,x 的深度为 dx(根深度为 1

但是到根路径上全部染黑后这个点不能再选的限制不好处理

假设我们依然能选这些点,但不计算贡献,将问题变得对称,这是常见的处理手段

考虑随便选择时的操作序列,选这些点并不影响 x 的期望选择次数,而且也不会影响所有点的颜色,不选这些点就是把这些不合法操作从操作序列中去掉,但没有造成任何影响

因此,原问题与新问题等价

这样就好办了,设当前已选 k 个点,有 kdx 的概率不会选新的,则期望 dxdxk 步选到下一个点

总共期望 k=0dx1dxdxk=dxi=1dx1i 步选完这条链

问题转化后,每个点被选的机会均等,所以 x 期望被选 i=1dx1i

答案为 i=1nHdi

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;
}

CF1392H ZS Shuffles Cards

做了这道题,就不再想说 「组合意义天地灭,数学推导保平安」

推式子我至今自己推不出来,CF 官方题解中的也非常复杂

但是可以用组合意义巧妙的解释这题

E(抽到所有牌的次数)=i=1P(在第 i-1 轮未结束)×E(第 i 轮抽的张数)

由于每轮均匀随机,期望抽到的张数相同,因此

=i=1P(在第 i-1 轮未结束)×E(一轮中抽的张数)=E(一轮中抽的张数)×i=1P(在第 i-1 轮未结束)

根据期望的公式,i=1P(在第 i-1 轮未结束)=E(轮数)

因此,

E(抽到所有牌的次数)=E(轮数)×E(一轮中抽的张数)

先算一轮中抽的张数,可以大力枚举张数计算概率,但期望有线性性,可以单独拿出一张数字牌考虑它期望被抽的次数,它在所有鬼牌前才能被抽,概率为 1m+1,那么共 n 张牌,最后还要抽一张鬼牌结束,期望张数为 nm+1+1

算轮数比较麻烦,设 fi 为当前还剩 i 张数字牌没拿到时的剩余期望轮数

由于拿到已经拿到的牌不会结束一轮也不会有贡献,可以直接忽略

所以每轮开头可以看作是新牌或鬼牌,把抽到一张新牌的所有轮记作一回合

但是抽到新牌后,一轮不会结束,有可能再抽到新牌,所以一回合中先去掉抽出新牌的那一轮,最后再加上令游戏结束的最后一轮

这样下一回合的开头紧接着上一回合的结尾,每轮有 mm+i 的概率开头为鬼牌,因此期望 m+ii 轮进入下一回合

去掉最后一轮后,fi=mi+fi1

E(轮数)=i=1nfi+1=mi=1n1i+1=mHn+1

答案即为 (mHn+1)(nm+1+1)

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;
}

高次期望

P1654 OSU!

solution

拆括号(x+1)3=x3+3x2+3x+1

考虑一下当新加入位置 i 时,相对 i1 会产生 3xi12+3xi1+1 的新增贡献,计算它即可

注意这里新增是相对上个位置填完后强制计算后缀的贡献后而言

1pi 的概率填 0,结束这一段,贡献不新增

pi 概率填 1,此时

  • 对于 x 产生 x1[i]=(x1[i1]+1)×p[i] 新增贡献

  • 对于 x2 产生 x2[i]=(x2[i1]+2x1[i1]+1)×p[i] 新增贡献

  • 对于 x3 即答案,f[i] 为到第 i 位的总贡献,产生 x3[i]=(3x2[i1]+3x1[i1]+1)×p[i] 新增贡献,但统计的是总贡献,所以还要加上到前一位的总贡献 f[i1]

所以 f[i]=f[i1]+(3x2[i1]+3x1[i1]+1)×p[i]

答案为 f[n]

小问题:为什么 x3[i] 的新增贡献中没有像 x1[i],x2[i] 那样加上前一项呢?

其实比较简单,因为对于最终答案来说,新增的是整个 xx2,式子后面加的是对于上一位时新增的 xx2 来说这一位xx2 新增的,但对上个 x3 而言只新增了 3x2+3x+1,前一项 x3 是到上一位的总和

即相当于拆开了每一位的贡献,把每一位的贡献累加起来

有点绕

code

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];
}

概率

常见公式

一些太基础的就不写了

条件概率公式:P(AB)=P(A|B)P(B)=P(B|A)P(A)

这里的 P(A|B) 指事件 AB 已经发生时发生的概率

全概率公式:如果一组事件 C1n 满足它们之间恰好有一个会发生,即 i=1nP(Ci)=1,那么

P(A)=i=1nP(ACi)=i=1nP(A|Ci)P(Ci)

常用的特例:P(A)=P(A|B)P(B)+P(A|B¯)P(B¯)

贝叶斯公式:可以由条件概率公式推导而来,P(A|B)=P(AB)P(B)=P(B|A)P(A)P(B)

概率分布

用于表述随机变量取值的概率规律

常见分布:

  • 两点分布,又称伯努利分布

X 只有 0,1 两种取值,有 p 的概率是 11p 的概率是 0

  • 几何分布,设在伯努利实验中,X 为得到一次成功需要的实验次数,那么 P[X=k]=(1p)k1p

它的期望 E[X]=1p,可以用等比数列求和推,也可以用 DP 推出

P9963 [THUPC 2024 初赛] 前缀和

场上并不知道分布

但是发现每个位置的期望大小相同为 E,且每个位置独立,答案是 rl+1E

然后由于精度要求不高,暴力算了数列前 5×105 项就算出了 E,过了

现在知道分布后,E=1p,答案就是 (rl+1)p

官方题解很妙:看作一排灯,每盏灯有 p 的概率亮起,序列中每个位置的大小就是第一盏亮着的灯的位置,然后把它后面的那盏灯记为第一盏,以此类推,得到后面位置的大小,那么问题转化为 [l,r] 内期望有多少盏亮着的灯,答案为 (rl+1)p

  • 二项分布,描述进行 n 次伯努利实验成功的次数,P[X=k]=(nk)pk(1p)nk

期望为 E[X]=np,还是比较好理解的,一次实验成功次数的期望是 p,期望有线性性

  • 超几何分布,描述抽样不放回,N 个产品中 K 个不合格,现在抽 n 个送检,设 X 为不合格样品数量

P[X=k]=(Kk)(NKnk)(Nn),总方案数除以选择方案数,期望 E[X]=nKN,证明比较复杂,注意虽然前一次抽取会影响样本空间,但概率权重会把影响抵消

树上处理双向贡献

P4284 [SHOI2014] 概率充电器

期望没什么用,实际求的是每个节点有电的概率之和

发现某个节点通电,要么它自己有电,要么它的父节点或子节点有电传给它

如果设 fi 表示 i 有电的概率,那么发现它要从父节点,子节点同时转移过来,没法做

考虑先只处理子节点,注意子节点 i 有电与 j 有电并不互斥,所以要用容斥原理算出 ij 通电的概率

这样根节点处的概率已经算对了

然后处理父节点,此时父节点通电的概率就要去掉从当前子树通电的情况

同样用容斥,设 P(A) 为 父节点不从当前节点通电的概率,P(B) 为当前节点到父节点有电的概率,P(B)=fy×px,y

P(A)+P(B)P(A)P(B)=fx,解方程得 P(A)=fxP(B)1P(B)

fy=fy+P(A)×px,yfy×P(A)×px,y,仍然是容斥原理计算

细节:如果 P(B)=1,则可以直接跳过,因为始终有电了,防止除以 0

#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;
}

轮数处理

P3239 [HNOI2015] 亚瑟王

把期望拆成每张卡牌发动的概率 × 分数,实际上是求概率

一轮一轮直接求肯定不好求,考虑从整体计算

如果有 r 轮决策到了卡牌 x,则都不发动的概率为 (1px)r,发动的概率为 1(1px)r

但是如果一轮中前面的卡牌有发动的,这一轮就轮不到它

f(i,j) 为前 i 张卡牌,发动了 j 张的概率

则第 i 张有 rj 轮轮到了它决策,分它发动和不发动两种情况转移

si 为第 i 张卡牌发动的总概率,则 si=j=0if(i1,j)×(1(1pi)rj)

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;
}

杂项

[AGC020F] Arcs on a Circle

很 NB 的概率题

一看可以覆盖实数,显然不好做

但线段长度均为整数,如果我们知道了它们小数部分的相对大小,则只用知道覆盖到哪个整数就能知道是否覆盖

n 很小,我们完全可以 O(n!) 枚举相对大小,排序后 DP

注意这里断环为链必须选取最长的放在起点,避免某条线段在最后时转了一圈完全覆盖起点

那么每个整数相当于拆成了 n 个数,第 i 个必须放在对应小数部分相对大小对应的点

fs,j 为集合 s 中的线段,覆盖了 [0,j] 的方案数

转移则先从小到大枚举下一条线段放置的位置,强制从前往后放,避免顺序问题,然后枚举覆盖到的位置和已经放置的集合,状压 DP

最后放置的总方案数为 (n1)!×cn1,因为除了第一个,共有 (n1)! 种大小顺序,其它线段都有 c 种放置方法

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;
}
posted @   KellyWLJ  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示