期望与概率dp
概率与期望dp
定义:
概率:事件A发生的可能性,计作P(A)
期望:事件A结果的平均大小,记住E(x)
E(x)=每种结果的大小与其概率的乘积的和
注意计算概率时需要考虑是否要用容斥原理
期望dp时注意有时要用倒序枚举
其实本质和其他的dp没什么区别
例题
概率充电器
题意:n个充电元件由n-1条导线连通,每个充电原件自身是否直接充电以及每条导线是否导电都由概率决定,求进入充电状态的元件个数的期望
1<=n<=500000
树形换根概率dp,注意使用容斥原理
第一遍dfs:计算出f[i]表示由子树i通电的概率
第二遍dfs:计算出从父亲节点来电的概率,再用容斥原理加在一起
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
#define maxn 500100
int n;
int fir[maxn],nxt[maxn*2],vv[maxn*2];
double edge[maxn*2],q[maxn];
int tot=0;
double ans=0;
void add(int u,int v,double w)
{
nxt[++tot]=fir[u];
fir[u]=tot;
vv[tot]=v;
edge[tot]=w;
}
double h[maxn];
void dfs1(int u,int fa)
{
h[u]=q[u];
for(int i=fir[u];i;i=nxt[i])
{
int v=vv[i];
if(v==fa)continue;
dfs1(v,u);
h[u]=h[u]+h[v]*edge[i]-h[u]*h[v]*edge[i];
}
}
void dfs2(int u,int fa)
{
for(int i=fir[u];i;i=nxt[i])
{
int v=vv[i];
if(v==fa)continue;
if(fabs(1-h[v]*edge[i])>(1e-7))
{
double t=(h[u]-h[v]*edge[i])/(1-h[v]*edge[i]);
h[v]=h[v]+t*edge[i]-h[v]*t*edge[i];
ans+=h[v];
}else ans+=1;
dfs2(v,u);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int a,b;double p;scanf("%d%d%lf",&a,&b,&p);
add(a,b,p/100);add(b,a,p/100);
}
for(int i=1;i<=n;i++)scanf("%lf",&q[i]),q[i]/=100;
dfs1(1,0);
ans=h[1];
dfs2(1,0);
printf("%0.6lf",ans);
return 0;
}
换教室
题意:小A的学校可以视为一个v个点的无向图,他有n门课程要按顺序上课,其中第i门课程要在节点ai进行,但还有一个备选地点bi。现在小A有m个申请机会,若申请第i门课,那么将有ki的概率使课程搬到bi进行。每门课最多申请一次,m次机会不必全部用完。他如何申请才能最小化在上课地点间移动的距离的期望值。求该期望值。
v<=300,n,m<=200
先用floyd求出任意两点的距离
再令f[i][j][0/1]表示前i节课申请m次且第i节课申请/不申请的最小期望值
注意double类型需要手动赋初值
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int n,m,v,e;
double f[2010][2010],dp[2010][2010][2],k[2010];
int c[2010],d[2010];
double mymin(double x,double y)
{
return x<y?x:y;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&v,&e);
for(int i=1;i<=n;i++)scanf("%d",&c[i]);
for(int i=1;i<=n;i++)scanf("%d",&d[i]);
for(int i=1;i<=n;i++)scanf("%lf",&k[i]);
for(int i=1;i<=v;i++)for(int j=1;j<=v;j++)f[i][j]=1e9;
for(int i=1;i<=v;i++)f[i][i]=0;
for(int i=1;i<=e;i++)
{
int a,b;double w;scanf("%d%d%lf",&a,&b,&w);
f[a][b]=f[b][a]=min(f[a][b],w);
}
for(int kk=1;kk<=v;kk++)
{
for(int i=1;i<=v;i++)
{
for(int j=1;j<=v;j++)
{
f[i][j]=f[j][i]=min(f[i][j],f[i][kk]+f[kk][j]);
}
}
}
for(int i=1;i<=n;i++)for(int j=0;j<=m;j++)
{
dp[i][j][0]=1e9;dp[i][j][1]=1e9;
}
dp[1][0][0]=dp[1][1][1]=0;
for(int i=2;i<=n;i++)
{
for(int j=0;j<=min(m,i);j++)
{
dp[i][j][0]=mymin(dp[i-1][j][0]+f[c[i]][c[i-1]],dp[i-1][j][1]+k[i-1]*f[c[i]][d[i-1]]+(1-k[i-1])*f[c[i]][c[i-1]]);
if(j!=0)
dp[i][j][1]=mymin(dp[i-1][j-1][0]+f[c[i-1]][c[i]]*(1-k[i])+f[c[i-1]][d[i]]*k[i],dp[i-1][j-1][1]+f[c[i-1]][c[i]]*(1-k[i])*(1-k[i-1])+f[c[i-1]][d[i]]*(1-k[i-1])*k[i]+f[d[i-1]][c[i]]*k[i-1]*(1-k[i])+f[d[i-1]][d[i]]*k[i-1]*k[i]);
}
}
double ans=1e9;
for(int i=0;i<=m;i++)
{
ans=mymin(ans,dp[n][i][0]);ans=mymin(ans,dp[n][i][1]);
}
printf("%0.2lf",ans);
return 0;
}
奖励关
[题目]
题意:有n轮游戏和m种宝物,每种宝物有分数Pi(可以为负),每轮游戏会等概率抛出一种宝物,你可以选择吃或不吃。第i种宝物还有一个限制集合Si,表示只有在Si中的宝物都吃过后,才能吃第i种宝物。
1<=n<=100 1<=m<=15
期望状压dp
f[i][S]表示在第1轮到第i−1轮内宝物是否取过的状态为S,第i轮到第n轮的最大期望得分,进行逆推。
采用逆推的原因如下:
如果要从当前状态S0转移到目标状态S1,那么直接f[i+1][s1]+=f[i][s0]/n的转移方法是错误的,因为f[i+1][n]不一定有n种被转移到的方式,有可能因为限制集合的原因,有些状态不能转移到f[i+1][s],但如果采用逆推,那么我们是从一个合法的状态转移到当前状态,就不存在这样的问题,大多数期望dp都可以采用逆推的方法。
那么逆推如何转移呢:
枚举第k种宝物,可以取或不取,
如果能取,f[i][s]+=max(f[i+1][s],f[i+1][s|(1<<(k-1))]+p[k])
如果不能取,f[i][s]+=f[i+1][s]
然后f[i][s]/=n
答案为f[1][0]
#include <iostream>
#include <cstdio>
using namespace std;
int n,k;
double f[110][1<<18],value[110];
int sta[110];
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=k;i++)
{
scanf("%lf",&value[i]);
int x;scanf("%d",&x);
while(x!=0)
{
sta[i]=sta[i]|(1<<(x-1));
scanf("%d",&x);
}
}
for(int i=n;i>=1;i--)
{
for(int j=0;j<=(1<<k)-1;j++)
{
for(int kk=1;kk<=k;kk++)
{
if((j|sta[kk])==j)
{
f[i][j]+=max(f[i+1][j],f[i+1][j|(1<<(kk-1))]+value[kk]);
}
else f[i][j]+=f[i+1][j];
}
f[i][j]=(double)f[i][j]*1.0/k;
}
}
printf("%.6lf",f[1][0]);
return 0;
}