概率期望小结

P4316 绿豆蛙的归宿

典型的期望 dp。思路就是反向建图反向跑 dp

式子是这样的:
\(\large dp[v]=\sum \frac{dp[u]+w[u \ to \ v]}{indeg[v]}\)

然后遍历图可以使用拓扑排序或者深搜。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
struct node{
	int to,next;
	double w;
}edge[200101];
int indeg[200100],dx[200001];
int head[201001],cnt;
double dp[200100];
void add(int u,int v,int w)
{
	edge[++cnt].next=head[u];
	edge[cnt].to=v;
	edge[cnt].w=w;
	head[u]=cnt;
}
void toposort()
{
	queue<int>q;
	q.push(n);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			dp[v]+=(dp[u]+edge[i].w)/dx[v];
			indeg[v]--;
			if(!indeg[v]) q.push(v);
		}
	}
}
signed main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int u,v,w;
		cin>>u>>v>>w;
		add(v,u,w);
		indeg[u]++,dx[u]++;
	}
	toposort();
	printf("%.2lf",dp[1]);
}

CF148D Bag of mice

概率 dp。

我们考虑一个状态:有 \(i\) 个白鼠和 \(j\) 个黑鼠

有以下几种特殊情况:

  • \(j=0\) 有 0 个黑鼠,胜率为 1;

  • \(j=1\) A 抓到则黑鼠则输,胜率显然为 \(\frac{i}{i+1}\)

否则:

  • A 抓到白鼠,概率为 \(\frac{i}{i+j}\)

  • A 抓到黑鼠,B 抓到黑鼠,跑一只白鼠:
    A 抓到黑鼠的概率是 \(\frac{j}{i+j}\),B 抓到黑鼠概率是 \(\frac{j-1}{i+j-1}\),跑一只白鼠的概率是 \(\frac{i}{i+j-2}\),还需要乘上 \(dp[i-1][j-2]\),代表去掉一只白鼠两只黑鼠后的胜率。
    总体概率 \(P=\frac{j}{i+j}\times \frac{j-1}{i+j-1}\times \frac{j-2}{i+j-2}\times dp[i][j-3]\)

  • A 抓到黑鼠,B 抓到黑鼠,跑一只黑鼠:
    这就和上面同理了,总体概率 \(P=\frac{j}{i+j}\times \frac{j-1}{i+j-1}\times \frac{j-2}{i+j-2}\times dp[i][j-3]\)

综上所述,对于有 \(i\) 个白鼠 \(j\) 个黑鼠,胜率为:

\[dp[i][j]=\frac{i}{i+j}+\frac{j}{i+j}\times \frac{j-1}{i+j-1}\times \frac{j-2}{i+j-2}\times dp[i][j-3]+\frac{j}{i+j}\times \frac{j-1}{i+j-1}\times \frac{j-2}{i+j-2}\times dp[i][j-3] \]

得解,直接 \(O(nm)\) 转移即可。

for(int i=1;i<=n;i++)
	dp[i][0]=1.0,dp[i][1]=1.0*i/(i+1);//初始化
for(int i=1;i<=n;i++)
{
	for(int j=2;j<=m;j++)
	{
		dp[i][j]=1.0*i/(i+j);//A 抽到白鼠
		dp[i][j]+=1.0*j/(i+j)*(j-1)/(i+j-1)*i/(i+j-2)*dp[i-1][j-2];//A 抽到黑鼠,B抽到黑鼠,跑一只白鼠
		if(j>2)	dp[i][j]+=1.0*j/(i+j)*(j-1)/(i+j-1)*(j-2)/(i+j-2)*dp[i][j-3];//A 抽到黑鼠,B 抽到黑鼠,跑一只黑鼠
	}
}

P1654 OSU!

递推期望。

如果当前答案为 \(X^3\),那么再加一位的答案就是 \((X+1)^3\)

\[\def\t {\times} \begin{aligned} &(x+1)^3 \\ =&(x+1)^2 \t (x+1) \\ =&(x^2+2x+1)\t(x+1) \\ =&x^3+x^2+2x^2+2x+x+1 \\ =&x^3+3x^2+3x+1 \end{aligned} \]

所以答案会增加 \(3x^2+3x+1\)

这里利用了期望的线性性,期望的和等于和的期望。

所以,我们再来分别维护 \(x^2\)\(x\) 的期望。

和上式同理,我们可以知道 \((x+1)^2=x^2+2x+1\)

得解。

#include<bits/stdc++.h>
using namespace std;
int n;
double x1[100001],x2[100001];
double dp[100001];
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		double x;
		cin>>x;
		x1[i]=(x1[i-1]+1)*x;
		x2[i]=(x2[i-1]+2*x1[i-1]+1)*x;
		dp[i]=dp[i-1]+(x1[i-1]*3+x2[i-1]*3+1)*x;
	}
	printf("%.1lf",dp[n]);
}

P1297 [国家集训队] 单选错位

期望题。

我们可以观察到,第 \(i+1\)个答案只与 \(i\) 有关。

  • \(a[i]=a[i+1]\) 时,每次选对的期望是 \(\frac{1}{a[i]}\)
  • \(a[i]>a[i+1]\) 时,有 \(\frac{a[i+1]}{a[i]}\) 的概率选到 \(a[i+1]\) 之内,并且每次选对 \(\frac{1}{a[i+1]}\),期望为 \(\frac{a[i+1]}{a[i]}\times \frac{1}{a[i+1]}=\frac{1}{a[i+1]}\)
  • \(a[i]<a[i+1]\) 时,正确答案只出现在 \(1~a[i]\) 中,股每次选出答案在其中的概率是 \(a[i]/a[i+1]\),选对概率 \(\frac{1}{a[i]}\),期望为 \(\frac{1}{a[i+1]}\)

结合以上所述,最终答案是\(\sum_{i=1}^{n}\frac{1}{\max(a[i],a[i+1])}\)

要初始化 \(a[n+1]=a[1]\)

#P2028. [bzoj1419]Red is good

这个和上面一道摸老鼠的题很像。

定于 \(dp[i][j]\) 表示摸到 \(i\) 个红牌和 \(j\) 个黑牌的期望,有以下两种情况:

  • 摸到红牌,概率为 \(\frac{i}{i+j}\),从 \(dp[i-1][j]\) 转移而来,期望是 \((dp[i][j]+1)\times \frac{i}{i+j}+dp[i-1][j]\)

  • 摸到黑牌,概率为 \(\frac{j}{i+j}\),从 \(dp[i][j-1]\) 转移而来,期望是 \((dp[i][j]-1)\times \frac{j}{i+j}+dp[i][j-1]\)
    合起来得到的状态转移方程是:

\[dp[i][j]=(dp[i][j]+1)\times \frac{i}{i+j}+dp[i-1][j]+(dp[i][j]-1)\times \frac{j}{i+j}+dp[i][j-1] \]

需要注意的是,可以随时停止翻牌,所以我们要加一个判断,如果 \(dp[i][j]<0\),我们让 \(dp[i][j]\) 归零。

需要初始化 \(dp[i][0]=i\),即没有黑牌时抽多少都是红。

并且在输出的时候要注意题目要求不四舍五入

#include<bits/stdc++.h>
using namespace std;
int n,m;
double dp[1001][1001];

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) dp[i][0]=i*1.0;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			dp[i][j]=(dp[i-1][j]+1.0)*i/(i+j)+(dp[i][j-1]-1.0)*j/(i+j);
			if(dp[i][j]<0) dp[i][j]=0;
		}
	}
	int zheng=(int)dp[n][m];
	dp[n][m]-=zheng;
	cout<<zheng<<".";
	for(int i=1;i<=6;i++)
	{
		dp[n][m]*=10;
		cout<<(int)dp[n][m];
		dp[n][m]-=(int)dp[n][m];
	}
}

这样我们就得到了一个 \(40\) 分的代码,因为题上的空间限制了 \(64MB\),需要优化空间。

我们注意到 \(dp[i][j]\) 的值只与 \(dp[i-1][j]\)\(dp[i][j-1]\) 有关,即每一行的循环只会用到两行 \(dp\) 数组,因此可以使用滚动数组

#include<bits/stdc++.h>
using namespace std;
int n,m;
double dp[2][5001];
int rev=1;
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		rev^=1;//滚一位
		dp[rev][0]=i;
		for(int j=1;j<=m;j++)
		{
			dp[rev][j]=(dp[rev^1][j]+1.0)*i/(i+j)+(dp[rev][j-1]-1.0)*j/(i+j);//rev^1 就是上一行的
			if(dp[rev][j]<0) dp[rev][j]=0;
		}
	}
	int zheng=(int)dp[rev][m];
	dp[rev][m]-=zheng;
	cout<<zheng<<".";
	for(int i=1;i<=6;i++)
	{
		dp[rev][m]*=10;
		cout<<(int)dp[rev][m];
		dp[rev][m]-=(int)dp[rev][m];
	}
}

#P2030. [bzoj2969]矩形粉刷

很难想啊。对我来说

对于一个点 \((i,j)\),选中的概率是 1-没选中的概率。

而对于没选中的概率,有以下几种情况:

  • 选中两点同时在这个点上方,概率是 \(\frac{i-1}{n}\times \frac{i-1}{n}=(\frac{i-1}{n})^2\)

  • 选中两点同时在这个点下方,概率是 \((\frac{n-i}{n})^2\)

  • 选中两点同时在这个点左方,概率是 \((\frac{j-1}{m})^2\);

  • 选中两点同时在这个点右方,概率是 \((\frac{m-j}{m})^2\)

运用二维前缀和的知识,我们可以知道会有左上,左下,右上,右下四个地方分别被多算了一次,因此,最终的式子是:

\[\def\t{\times} dp[i][j]=(\frac{i-1}{n})^2+(\frac{n-i}{n})^2+(\frac{j-1}{m})^2+(\frac{m-j}{m})^2-\frac{i-1}{n}\t\frac{m-j}{m}-\frac{n-i}{n}\t\frac{m-j}{m}-\frac{n-i}{n}\t\frac{j-1}{m}-\frac{i-1}{n}\t\frac{j-1}{m} \]

最后计算的时候,需要先算 \(k\) 次的概率后 -1。

#include<bits/stdc++.h>
using namespace std;
int n,m,k;
double dp[1001][1001];
double ppow(double a,int b)
{
	double res=1;
	while(b--)
	res*=a;
	return res;
}
int main()
{
	cin>>k>>n>>m;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			dp[i][j]=ppow(1.0*(i-1.0)/n,2)+ppow(1.0*(n-i)/n,2)
			+ppow(1.0*(j-1.0)/m,2)+ppow(1.0*(m-j)/m,2)
			-ppow(1.0*(i-1.0)/n,2)*ppow(1.0*(j-1.0)/m,2)
			-ppow(1.0*(n-i)/n,2)*ppow(1.0*(m-j)/m,2)
			-ppow(1.0*(i-1.0)/n,2)*ppow(1.0*(m-j)/m,2)
			-ppow(1.0*(n-i)/n,2)*ppow(1.0*(j-1.0)/m,2);
		}
	}
	double res=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
			res+=1-(ppow(dp[i][j],k));
	}
	printf("%.0lf",res);return 0;
}

P2059 [JLOI2013] 卡牌游戏

神秘题目,在一月写过,但是看代码没有任何印象。

甚至题解里的没一个和我想法一样…………

考虑从只有两个人的状态开始倒推:

当只有两人的情况,假设牌有 \(2,3,5,7\)\(4\) 张,庄为 1。如果庄抽到 2,将淘汰对方,概率为 \(\frac{1}{m}=\frac{1}{4}\),否则,将会淘汰自己,概率为 \(\frac{3}{4}\)

二者都能从上一个状态:胜者的概率 1 转移而来。

可以理解为: 所有的 当前人进入下一轮的位置的概率 \(\times\frac{1}{m}\) 就是钦定的这个人(与题目编号无关)获胜的概率。

类比一下,当剩下 3 个人的时候,循环这 3 个人,并循环这 \(m\) 张卡。

找到当第 \(i\) 张牌被抽到时,每个人对应的只有两个人的状态的位置,乘上概率 \(\frac{1}{m}\),就可得到答案。

太抽象了!

用代码解释一下(浅浅滚了一下):

#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[51];
double dp[2][51];
int r;
int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		cin>>a[i];
	}
	dp[0][1]=1.0;//只有一个人的时候胜率就是 1
	for(int i=2;i<=n;i++)//从只有两个人的状态开始转移
	{
		r^=1;//滚
		for(int j=1;j<=i;j++)
		{
			dp[r][j]=0;
		}
		for(int j=1;j<=m;j++)//循环 m 张牌
		{
			for(int k=1,p=(a[j]-1)%i+1;k<i;k++)
			{//这是最抽象的部分。
			//k 代表下一轮的位置,因为下一轮是我们上一个循环 i 求得的,所以可以转移。
			//p 代表下一轮第 k 个位置对应当前轮的哪一个
				p++;//加之前的第 p 个就是被干掉的
				if(p>i) p%=i;
				dp[r][p]+=dp[r^1][k]/(double)m;//转移,当前第 p 个人的概率由上一轮对应位置的人的概率转移而来,并要乘上抽到这张牌的概率 1/m
				//因为第 p 个人有可能在抽到很多张牌时存活,所以概率叠加。
			}
		}
	}
	for(int i=1;i<=n;i++)
	{
		printf("%.2lf%% ",dp[r][i]*100);
	}
}

CF235B Let's Play Osu!

这个和上一个 osu 几乎一样。

维护两个数组分别表示一次和二次。

每次的状态转移是这样的:

\(dp[i]=(dp[i-1]+2\times f[i-1])\times p+dp[i-1]\times(1-p)\)

其中 \(f[i]\) 每次需要更新成 \((f[i-1]+1)\times p\)

当然我们也可以把数组换成两个变量。

P6154 游走

在 DAG 上面跑拓扑排序,全部的期望是 \(\frac{\text{路径长度}}{\text{路径条数}}\)

考虑 \(f(i)\) 表示以 \(i\) 为终点的路径总长度,\(g(i)\) 表示以 \(i\) 为终点的路径条数,我们可以发现:

\(i,j\) 有一条边时,以 \(j\) 为终点的路径条数会加上 \(g(i)\),所有以 \(j\) 为终点的路径长度都会加一,而这种路径有 \(g(i)\) 条,因此 \(f(j)+=g(i)+f(i)\)

最终的答案就是\(\displaystyle\frac{\sum_{i=1}^{n}{f(i)}}{\sum_{i=1}^{n}{sum(i)}}\)

分母用逆元求。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
struct node{
	int to,next,w;
}edge[700010];
const int mod=998244353;
int head[100001],cnt;
void add(int u,int v)
{
	edge[++cnt].w=1;
	edge[cnt].to=v;
	edge[cnt].next=head[u];
	head[u]=cnt;
}
int indeg[100001],root;
int f[100001],num[100010];
queue<int>q;
inline void toposort()
{
	while(!q.empty())
	{
		register int u=q.front();q.pop();
		for(register int i=head[u];i;i=edge[i].next)
		{
			register int v=edge[i].to;
			num[v]+=num[u];num[v]%=mod;
			f[v]+=(f[u]+num[u])%mod;
			indeg[v]--;
			if(indeg[v]==0)q.push(v);
		}
	}
}
inline int ppow(int a,int b)
{
	register int res=1;
	while(b)
	{
		if(b&1) res=(res*a)%mod;
		a=(a*a)%mod;b>>=1;
	}return res;
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(register int i=1;i<=m;++i)
	{
		int u,v;
		cin>>u>>v;
		add(u,v);
		indeg[v]++;
	}
	for(register int i=1;i<=n;++i)
	{
		num[i]=1;
		if(!indeg[i]) q.push(i);
	}
	toposort();
//	for(int i=1;i<=n;i++) cout<<f[i]<<" "<<num[i]<<endl;
	register int ans1=0,ans2=0;
	for(register int i=1;i<=n;++i)
	{
		ans2=(ans2+num[i])%mod,ans1=(ans1+f[i])%mod;
	}
//	cout<<ans1<<" "<<ans2<<endl;
	cout<<(ans1*ppow(ans2,mod-2))%mod;
}

P4284 [SHOI2014] 概率充电器

这个题有 3 个部分。

考虑每个节点的电能从它的子树、它自己、它的子树外面得到,我们这样讨论:

电来自自己

期望就是自己能通电的概率。因此我们初始化 \(dp\) 数组就是用 \(\frac{q_{i}}{100}\)

电来自子树

树形 dp,来自子树与自己导电的和概率就要 用和概率公式

\(P(A\cup B)=P(A)+P(B)-P(A\cap B)\)

所以,我们可以推导出:

\(dp[u]=dp[v]\times p_{i}+dp[u]-(dp[v]\times p_{i}\times dp[u])\)

其中 \(p_{i}\) 表示 \(u,v\) 路径导电的概率。

电来自子树外面

我们令 \(P(A)\) 表示这个点子树外来电的概率(包含自己有电),\(P(B)\) 表示这个点子树来电的概率,可以得出:

\(P(A)+P(B)-P(A)P(B)=dp[u]\)

这里的 \(dp[u]\) 就是最终的期望了。

因为要求的是 \(P(A)\),我们把上式变化一下:

\(\displaystyle P(A)=\frac{dp[u]-P(B)}{1-P(B)}\)

这样就能得到答案了。

需要注意的是,从子树转移而来的 \(P(B)\)(实际上就是还未更新的 \(dp[v]\))需要乘上边的概率,最终 \(P(A)\) 也要乘上边的概率。

那么第二次更新就是:
\(\displaystyle dp[v]=dp[v]+\frac{dp[u]-dp[v]\times p_{i}}{1-dp[v]\times p_{i}}\times p_{i}-(dp[v]\times \frac{dp[u]-dp[v]\times p_{i}}{1-dp[v]\times p_{i}}\times p_{i})\)

最终答案是 \(\displaystyle\sum_{i=1}^{n}dp[i]\)

#include<bits/stdc++.h>
using namespace std;
int n;
const int N=5e5+10;
struct node{
	int to,next,w;
}edge[N<<1];
int head[N],cnt;
void add(int u,int v,int w)
{
	edge[++cnt].next=head[u];
	edge[cnt].to=v;
	edge[cnt].w=w;
	head[u]=cnt;
}
int a[N],fa[N];
long double dp[N];
void dfs(int u)
{
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa[u]) continue;
		fa[v]=u,dfs(v);
		long double k=dp[v]*(edge[i].w*0.01);
		dp[u]=(dp[u]+k)-(dp[u]*k);
	}
}
long double ans;
void dfs1(int u)
{
	ans+=dp[u]; 
	for(int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa[u]) continue;
		if((1-dp[v])<=1e-7)
		{
			dfs1(v);continue;
		}
		long double k=(dp[u]-dp[v]*double(edge[i].w*0.01))/(1.0-dp[v]*double(edge[i].w*0.01))*double(edge[i].w*0.01);
		dp[v]=dp[v]+k-(dp[v]*k);
		dfs1(v);
	}
}
int main()
{
	cin>>n;
	for(int i=1;i<n;i++)
	{
		int u,v,w;
		cin>>u>>v>>w;
		add(u,v,w),add(v,u,w);
	}
	for(int i=1;i<=n;i++)
		cin>>a[i],dp[i]=a[i]*0.01;
	dfs(1);
	dfs1(1);
	printf("%.6Lf",ans);
}
posted @ 2024-02-26 21:46  ccjjxx  阅读(23)  评论(0)    收藏  举报