概率期望小结
P4316 绿豆蛙的归宿
典型的期望 dp。思路就是反向建图加反向跑 dp。
式子是这样的:
然后遍历图可以使用拓扑排序或者深搜。
#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。
我们考虑一个状态:有 个白鼠和 个黑鼠
有以下几种特殊情况:
-
有 0 个黑鼠,胜率为 1;
-
A 抓到则黑鼠则输,胜率显然为 ;
否则:
-
A 抓到白鼠,概率为
-
A 抓到黑鼠,B 抓到黑鼠,跑一只白鼠:
A 抓到黑鼠的概率是 ,B 抓到黑鼠概率是 ,跑一只白鼠的概率是 ,还需要乘上 ,代表去掉一只白鼠两只黑鼠后的胜率。
总体概率 。 -
A 抓到黑鼠,B 抓到黑鼠,跑一只黑鼠:
这就和上面同理了,总体概率 。
综上所述,对于有 个白鼠 个黑鼠,胜率为:
得解,直接 转移即可。
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!
递推期望。
如果当前答案为 ,那么再加一位的答案就是 。
所以答案会增加 。
这里利用了期望的线性性,期望的和等于和的期望。
所以,我们再来分别维护 和 的期望。
和上式同理,我们可以知道 。
得解。
#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 [国家集训队] 单选错位
期望题。
我们可以观察到,第 个答案只与 有关。
- 时,每次选对的期望是 ;
- 时,有 的概率选到 之内,并且每次选对 ,期望为 ;
- 时,正确答案只出现在 中,股每次选出答案在其中的概率是 ,选对概率 ,期望为 。
结合以上所述,最终答案是。
要初始化 。
#P2028. [bzoj1419]Red is good
这个和上面一道摸老鼠的题很像。
定于 表示摸到 个红牌和 个黑牌的期望,有以下两种情况:
-
摸到红牌,概率为 ,从 转移而来,期望是 ;
-
摸到黑牌,概率为 ,从 转移而来,期望是 。
合起来得到的状态转移方程是:
需要注意的是,可以随时停止翻牌,所以我们要加一个判断,如果 ,我们让 归零。
需要初始化 ,即没有黑牌时抽多少都是红。
并且在输出的时候要注意题目要求不四舍五入。
#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]; } }
这样我们就得到了一个 分的代码,因为题上的空间限制了 ,需要优化空间。
我们注意到 的值只与 和 有关,即每一行的循环只会用到两行 数组,因此可以使用滚动数组。
#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]矩形粉刷
很难想啊。对我来说
对于一个点 ,选中的概率是 1-没选中的概率。
而对于没选中的概率,有以下几种情况:
-
选中两点同时在这个点上方,概率是 ;
-
选中两点同时在这个点下方,概率是 ;
-
选中两点同时在这个点左方,概率是 ;
-
选中两点同时在这个点右方,概率是 。
运用二维前缀和的知识,我们可以知道会有左上,左下,右上,右下四个地方分别被多算了一次,因此,最终的式子是:
最后计算的时候,需要先算 次的概率后 -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] 卡牌游戏
神秘题目,在一月写过,但是看代码没有任何印象。
甚至题解里的没一个和我想法一样…………
考虑从只有两个人的状态开始倒推:
当只有两人的情况,假设牌有 这 张,庄为 1。如果庄抽到 2,将淘汰对方,概率为 ,否则,将会淘汰自己,概率为 。
二者都能从上一个状态:胜者的概率 1 转移而来。
可以理解为: 所有的 当前人进入下一轮的位置的概率 就是钦定的这个人(与题目编号无关)获胜的概率。
类比一下,当剩下 3 个人的时候,循环这 3 个人,并循环这 张卡。
找到当第 张牌被抽到时,每个人对应的只有两个人的状态的位置,乘上概率 ,就可得到答案。
太抽象了!
用代码解释一下(浅浅滚了一下):
#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 几乎一样。
维护两个数组分别表示一次和二次。
每次的状态转移是这样的:
其中 每次需要更新成 。
当然我们也可以把数组换成两个变量。
P6154 游走
在 DAG 上面跑拓扑排序,全部的期望是 。
考虑 表示以 为终点的路径总长度, 表示以 为终点的路径条数,我们可以发现:
当 有一条边时,以 为终点的路径条数会加上 ,所有以 为终点的路径长度都会加一,而这种路径有 条,因此 。
最终的答案就是
分母用逆元求。
#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,来自子树与自己导电的和概率就要 用和概率公式:
所以,我们可以推导出:
其中 表示 路径导电的概率。
电来自子树外面
我们令 表示这个点子树外来电的概率(包含自己有电), 表示这个点子树来电的概率,可以得出:
这里的 就是最终的期望了。
因为要求的是 ,我们把上式变化一下:
这样就能得到答案了。
需要注意的是,从子树转移而来的 (实际上就是还未更新的 )需要乘上边的概率,最终 也要乘上边的概率。
那么第二次更新就是:
最终答案是 。
#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); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!