概率期望小结
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\) 个黑鼠,胜率为:
得解,直接 \(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\)。
所以答案会增加 \(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]<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\)。
运用二维前缀和的知识,我们可以知道会有左上,左下,右上,右下四个地方分别被多算了一次,因此,最终的式子是:
最后计算的时候,需要先算 \(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);
}
浙公网安备 33010602011771号