概率期望小结

P4316 绿豆蛙的归宿

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

式子是这样的:
dp[v]=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 抓到则黑鼠则输,胜率显然为 ii+1

否则:

  • A 抓到白鼠,概率为 ii+j

  • A 抓到黑鼠,B 抓到黑鼠,跑一只白鼠:
    A 抓到黑鼠的概率是 ji+j,B 抓到黑鼠概率是 j1i+j1,跑一只白鼠的概率是 ii+j2,还需要乘上 dp[i1][j2],代表去掉一只白鼠两只黑鼠后的胜率。
    总体概率 P=ji+j×j1i+j1×j2i+j2×dp[i][j3]

  • A 抓到黑鼠,B 抓到黑鼠,跑一只黑鼠:
    这就和上面同理了,总体概率 P=ji+j×j1i+j1×j2i+j2×dp[i][j3]

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

dp[i][j]=ii+j+ji+j×j1i+j1×j2i+j2×dp[i][j3]+ji+j×j1i+j1×j2i+j2×dp[i][j3]

得解,直接 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!

递推期望。

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

(x+1)3=(x+1)2×(x+1)=(x2+2x+1)×(x+1)=x3+x2+2x2+2x+x+1=x3+3x2+3x+1

所以答案会增加 3x2+3x+1

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

所以,我们再来分别维护 x2x 的期望。

和上式同理,我们可以知道 (x+1)2=x2+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] 时,每次选对的期望是 1a[i]
  • a[i]>a[i+1] 时,有 a[i+1]a[i] 的概率选到 a[i+1] 之内,并且每次选对 1a[i+1],期望为 a[i+1]a[i]×1a[i+1]=1a[i+1]
  • a[i]<a[i+1] 时,正确答案只出现在 1 a[i] 中,股每次选出答案在其中的概率是 a[i]/a[i+1],选对概率 1a[i],期望为 1a[i+1]

结合以上所述,最终答案是i=1n1max(a[i],a[i+1])

要初始化 a[n+1]=a[1]

#P2028. [bzoj1419]Red is good

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

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

  • 摸到红牌,概率为 ii+j,从 dp[i1][j] 转移而来,期望是 (dp[i][j]+1)×ii+j+dp[i1][j]

  • 摸到黑牌,概率为 ji+j,从 dp[i][j1] 转移而来,期望是 (dp[i][j]1)×ji+j+dp[i][j1]
    合起来得到的状态转移方程是:

dp[i][j]=(dp[i][j]+1)×ii+j+dp[i1][j]+(dp[i][j]1)×ji+j+dp[i][j1]

需要注意的是,可以随时停止翻牌,所以我们要加一个判断,如果 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[i1][j]dp[i][j1] 有关,即每一行的循环只会用到两行 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-没选中的概率。

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

  • 选中两点同时在这个点上方,概率是 i1n×i1n=(i1n)2

  • 选中两点同时在这个点下方,概率是 (nin)2

  • 选中两点同时在这个点左方,概率是 (j1m)2;

  • 选中两点同时在这个点右方,概率是 (mjm)2

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

dp[i][j]=(i1n)2+(nin)2+(j1m)2+(mjm)2i1n×mjmnin×mjmnin×j1mi1n×j1m

最后计算的时候,需要先算 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,74 张,庄为 1。如果庄抽到 2,将淘汰对方,概率为 1m=14,否则,将会淘汰自己,概率为 34

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

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

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

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

太抽象了!

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

#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[i1]+2×f[i1])×p+dp[i1]×(1p)

其中 f[i] 每次需要更新成 (f[i1]+1)×p

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

P6154 游走

在 DAG 上面跑拓扑排序,全部的期望是 路径长度路径条数

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

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

最终的答案就是i=1nf(i)i=1nsum(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 数组就是用 qi100

电来自子树

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

P(AB)=P(A)+P(B)P(AB)

所以,我们可以推导出:

dp[u]=dp[v]×pi+dp[u](dp[v]×pi×dp[u])

其中 pi 表示 u,v 路径导电的概率。

电来自子树外面

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

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

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

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

P(A)=dp[u]P(B)1P(B)

这样就能得到答案了。

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

那么第二次更新就是:
dp[v]=dp[v]+dp[u]dp[v]×pi1dp[v]×pi×pi(dp[v]×dp[u]dp[v]×pi1dp[v]×pi×pi)

最终答案是 i=1ndp[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 @   ccjjxx  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示