学习笔记——概率期望
P4316 绿豆蛙的归宿
拓扑排序+概率期望
概率期望题一般有两种做题方式:正推,逆推
对于终止状态确定的一般用逆推
对于起始状态确定的一般用正推
- 逆推
这道题中,定义dp数组 \(f_i\) 表示从 \(i\) 到终点的路径长度期望,显然,\(f_n=0\),可以采取逆推的策略,式子如下:
- 正推
如果定义dp数组 \(f_i\) 表示从1到 \(i\) 的路径长度期望,则 \(f_1=0\),可以进行正推
然而我不会,逃
P4206 聪聪与可可
奇妙的预处理
因为点数极小,所以可以直接bfs预处理最短路,来预处理聪聪每次往哪里走,然后记忆化搜索每次可可随机走的情况即可
Code
#include<bits/stdc++.h>
#define N 1005
using namespace std;
int n,E,c,m;
int dis[N][N],vis[N][N],step[N][N],v[N][N];
struct edge{
int v,nxt;
}e[N*2];
int head[N],tot,deg[N];
double dp[N][N];
inline void add(int u,int v);
void bfs(int x);
double dfs(int i,int j);
int main(){
cin>>n>>E;
cin>>c>>m;
for(int i=1;i<=E;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);add(v,u);
deg[u]++,deg[v]++;
}
memset(dis,0x7f,sizeof(dis));
for(int i=1;i<=n;i++) bfs(i);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
int mem=0;
for(int k=head[i];k;k=e[k].nxt){
if(dis[j][e[k].v]<dis[j][mem])
mem=e[k].v;
if(dis[j][e[k].v]==dis[j][mem])
mem=min(mem,e[k].v);
}
step[i][j]=mem;
}
printf("%.3lf",dfs(c,m));
}
inline void add(int u,int v){
e[++tot].v=v;
e[tot].nxt=head[u];
head[u]=tot;
}
void bfs(int x){
queue<int> q;
q.push(x);
dis[x][x]=0;
while(!q.empty()){
int u=q.front();q.pop();
vis[x][u]=true;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v;
if(!vis[x][v]){
vis[x][v]=true;
dis[x][v]=dis[x][u]+1;
q.push(v);
}
}
}
}
double dfs(int i,int j){
if(v[i][j]) return dp[i][j];
v[i][j]=true;
if(i==j) return dp[i][j]=0;
int fir=step[i][j],sec=step[fir][j];
if(fir==j||sec==j) return dp[i][j]=1;
double sum=0;
for(int k=head[j];k;k=e[k].nxt)
sum+=dfs(sec,e[k].v);
sum+=dfs(sec,j);
return dp[i][j]=sum/(double)(deg[j]+1)+(double)1;
}
P1654 OSU!
这道题要维护 \(\sum E(x^3)\),但明显 \(E(x^3)\ne E(x)^3\)
逐步考虑 \(E(x),E(x^2)\)
考虑状态转移方程
维护三个变量转移即可,这里要注意最后一个的期望与前两个的独立
Code
#include<bits/stdc++.h>
#define N 100005
using namespace std;
int n;
double p,x1,x2,x3;
int main(){
cin>>n;
for(int i=1;i<=n;++i){
scanf("%lf",&p);
x3=x3+(3*x2+3*x1+1)*p;
x2=(x2+2*x1+1)*p;
x1=(x1+1)*p;
}
printf("%.1lf",x3);
}
Red is good
题面:
桌面上有R张红牌和B张黑牌,随机打乱顺序后放在桌面上,开始一张一张地翻牌,翻到红牌得到1美元,黑牌则付出1美元。可以随时停止翻牌,在最优策略下平均能得到多少钱。
设dp数组 \(f_{i,j}\) 表示有 \(i\) 个红牌 \(j\) 个黑牌时候的最优期望
\(f_{i,j}\) 可以由 \(f_{i-1,j},f_{i,j-1}\) 推出:
意义为最后一张翻的牌为红牌或黑牌对答案的影响
这道题的关键在于“最优策略”是什么,推状态转移方程的途中可以看出来,如果 \(f_{i,j}<0\) 的话,其实在采取最优策略的情况下已经不能够保证正收益了,所以不拿为上,直接令 \(f_{i,j}=0\)
Code
#include<bits/stdc++.h>
#define N 5005
using namespace std;
int r,b;
double dp[N][N];
int main(){
cin>>r>>b;
for(int i=0;i<=r;i++)
for(int j=0;j<=b;j++){
if(i>0) dp[i][j]+=(double)i/(double)(i+j)*(dp[i-1][j]+1);
if(j>0) dp[i][j]+=(double)j/(double)(i+j)*(dp[i][j-1]-1);
if(dp[i][j]<0) dp[i][j]=0;
}
printf("%.6lf",dp[r][b]-0.0000005);
}
守卫者的挑战
题面:
有n次挑战,每次挑战获胜可以得到一个地图碎片值为-1 或者 可以得到一个包包用来装地图碎片,最开始有一个包,每个挑战有一个获胜概率,现在让你求至少获胜L轮,挑战完n轮后能用背包装下地图碎片的概率
因为最多挑战 \(n\) 轮,所以最多获得 \(n\) 个碎片 \(n\le 200\),所以包包容量数太大的话完全没有用的,统一取成n即可
设计dp数组 \(f_{i,j,k}\) 为挑战 \(i\) 次,获胜 \(j\) 次,目前包包剩余容量为 \(k\) 的概率(可以为负),这里就出现了数组下标为负的情况,下标整体加上 \(n\) 即可
状态转移方程直接列出来,不解释:
Code
#include<bits/stdc++.h>
#define N 205
using namespace std;
int n,l,k,a[N];
double p[N],dp[N][N][N*2],ans;
int main(){
cin>>n>>l>>k;
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
p[i]=x/100.0;
}
for(int i=1;i<=n;i++)
scanf("%d",a+i);
k=min(n,k);
dp[0][0][n+k]=1;
for(int i=0;i<n;i++)
for(int j=0;j<=i;j++)
for(int h=-i;h<=n;h++){
dp[i+1][j][h+n]+=dp[i][j][h+n]*(1-p[i+1]);
dp[i+1][j+1][min(h+a[i+1],n)+n]+=dp[i][j][h+n]*p[i+1];
}
for(int i=l;i<=n;i++)
for(int j=n;j<=n*2;j++)
ans+=dp[n][i][j];
printf("%.6lf",ans);
}
P1297 单选错位
水题,古典概型
因为权值都为1,所以期望直接当概率算就行
对于 \(i\) 位置的选项迁移到 \(i+1\) 位置,两题选项的情况总数为 \(a_i\cdot a_{i+1}\),而两题答案相同的情况数为 \(\min\{a_i,a_{i+1}\}\),所以做对的概率为 \(\dfrac{\min\{a_i,a_{i+1}\}}{a_i\cdot a_{i+1}}=\dfrac{1}{\max\{a_i,a_{i+1}\}}\),答案累加一下即可
列队春游
题面:
每个小朋友视野距离的期望值根据定义显然有(\(p(i)\) 为视野距离为 \(i\) 的概率):
这一步是非常妙的(然而做这道题的时候根本没想把定义式展开)
然后将上式展开,设第 \(i\) 个小朋友有 \(k\) 个人能挡住他(即高度不小于他的人数):
对上述式子的解释:挡不住当前小朋友的人的排列显然对答案无影响,只用考虑能挡住当前小朋友的人即可,所以能挡住当前小朋友的人(算上小朋友本身)的排列数就是全部情况,而如果让当前小朋友的视野 \(\ge i\) 的话,能挡住当前小朋友的人就不能在小朋友前方 \(i-1\) 的地方出现,所以他们能在的地方只有 \(n-i\) 处,而小朋友本身又可以在 \(n-(i-1)=n-i+1\) 处位置出现,所以满足当前小朋友的视野 \(\ge i\) 的方案数为 \((n-i+1)A^{k}_{n-i}\)
然后就需要推个式子:
对倒数第二行推导的解释:
然后就可以 \(O(n)\) 直接做了
矩形粉刷
题面:
为了庆祝新的一年到来,小M决定要粉刷一个大木板。大木板实际上是一个W*H的方阵。小M得到了一个神奇的工具,这个工具只需要指定方阵中两个格子,就可以把这两格子为对角的,平行于木板边界的一个子矩形全部刷好。小M乐坏了,于是开始胡乱地使用这个工具。
假设小M每次选的两个格子都是完全随机的(方阵中每个格子被选中的概率是相等的),而且小M使用了K次工具,求木板上被小M粉刷过的格子个数的期望值是多少。
因为期望的权值为1,所以答案可以转化为每个格子被粉刷过(即被矩形包含)的概率之和,但是这样统计明显不可做(因为要考虑重复粉刷的情况),正难则反,统计每个格子不被粉刷的概率再用1减去即可
不难发现,如果选取的这两个点都在当前格子的上边/下边/左边/右边时就不会粉刷到当前格子,对于两个选取的点都在以当前格子为中心的四个角里的话,会重复计算,减去即可
P2059 卡牌游戏
这道题可以用记忆化搜索水过去:
dp数组 \(f_{i,j,k}\) 的定义为当进行到第 \(i\) 轮时轮到了第 \(j\) 个人且当前庄家为 \(k\) 的情况下第 \(j\) 个人的胜率
如果进行到最后一轮只剩下他自己的时候,胜率显然为1,以此为终止条件逐步模拟选牌过程转移即可
Code
#include<bits/stdc++.h>
#define N 55
#define db double
using namespace std;
int n,m,dat[N];
db dp[N][N][N],tmp;//还剩i个人时第j人庄家为k的胜率
db dfs(int i,int j,int k);
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++)
scanf("%d",dat+i);
dp[1][0][0]=1.0;
for(int i=1;i<=n;i++)
printf("%.2lf%% ",dfs(n,i-1,0)*100);
}
//g++ game.cpp -o test -std=c++14 -O2 -lm
db dfs(int i,int j,int k){
if(dp[i][j][k]) return dp[i][j][k];
if(i==1) return 0.0;
for(int l=1;l<=m;l++){
int pos=(k+dat[l]-1)%i;
if(pos==j) continue;
if(pos<j) dp[i][j][k]+=dfs(i-1,j-1,pos%(i-1))/(db)m;
if(pos>j) dp[i][j][k]+=dfs(i-1,j,pos%(i-1))/(db)m;
}
return dp[i][j][k];
}
其实还有一种直接dp的做法,其实道理是一样的,只是减去了庄家是谁的一维,而默认庄家为第一个人,做法不再赘述
P1850 换教室
想到最后才发现是道线性dp,然后切了
首先点数十分的小,所以可以直接Floyd求全源最短路
dp数组 \(f_{i,j,0/1}\) 的定义为已经决定了前 \(i\) 个的申请情况,已经申请了 \(j\) 个,第 \(i\) 个申请了 / 没申请 的期望路程
然后分类讨论:
- 对于第三维为0的情况
-
如果前一个未申请,则对答案的影响为 \(dis_{c_{i},c_{i-1}}\)
-
如果前一个申请了,则有两种情况:
- 前一个申请上了 对答案的影响为 $ dis_{d_{i-1},c_i} $,再乘上对应的概率 \(p_{i-1}\)
- 前一个没申请上 对答案的影响为 \(dis_{c_{i},c_{i-1}}\),概率为 \(1-p_{i-1}\)
- 对于第三维为1的情况讨论有点多,与上面相似,不再赘述
Code
#include<bits/stdc++.h>
#define N 2005
#define V 305
#define db double
using namespace std;
int n,m,v,e;
int c[N],d[N];
db p[N],dp[N][N][2],mat[V][V];
int main(){
cin>>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",p+i);
memset(mat,0x7f,sizeof(mat));
for(int i=1;i<=e;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
mat[u][v]=min(mat[u][v],(db)w);
mat[v][u]=min(mat[v][u],(db)w);
}
for(int i=1;i<=v;i++)
mat[i][i]=0;
for(int k=1;k<=v;k++)
for(int i=1;i<=v;i++)
for(int j=1;j<=v;j++)
mat[i][j]=min(mat[i][j],mat[i][k]+mat[k][j]);
memset(dp,0x7f,sizeof(dp));
dp[1][0][0]=0;
dp[1][1][1]=0;
for(int i=2;i<=n;i++)
for(int j=0;j<=m;j++){
dp[i][j][0]=min({dp[i][j][0],dp[i-1][j][0]+mat[c[i]][c[i-1]],dp[i-1][j][1]+p[i-1]*mat[c[i]][d[i-1]]+(1.0-p[i-1])*mat[c[i]][c[i-1]]});
if(j) dp[i][j][1]=min({dp[i][j][1],dp[i-1][j-1][0]+p[i]*mat[c[i-1]][d[i]]+(1.0-p[i])*mat[c[i-1]][c[i]],dp[i-1][j-1][1]+p[i]*p[i-1]*mat[d[i-1]][d[i]]+p[i-1]*(1.0-p[i])*mat[d[i-1]][c[i]]+p[i]*(1.0-p[i-1])*mat[c[i-1]][d[i]]+(1.0-p[i])*(1.0-p[i-1])*mat[c[i-1]][c[i]]});
}
db ans=0x3f3f3f3f3f3f3f;
for(int i=0;i<=m;i++)
ans=min(ans,min(dp[n][i][1],dp[n][i][0]));
printf("%.2lf",ans);
}
//g++ class.cpp -o test -std=c++14 -O2 -lm
P2473 奖励关
当时确实没有想到可以逆推(原因是找不到合适的dp数组定义)
一看数据范围显然是状压,顺推显然会导致一些考虑重复和后效性的问题,所以逆推
dp数组 \(f_{i,j}\) 定义为从第 \(i\) 轮选到第 \(k\) 轮的最优期望,且前 \(i-1\) 轮选的宝物集合为 \(j\),这样可以不用考虑多种情况,\(f_{1,0}\) 即为答案
这显然倒着转移,有两种情况:
- 当前的 \(j\) 能够满足当前宝物的前置条件,可以选择选不选该宝物
- 如果满足不了只能继承前面的结果
这样处理就不会有考虑当前该不该选此宝物的问题,顺推需要考虑选当前宝物对之后的影响,逆推则是先选了此宝物再看后面怎么选来满足此条件,在处理这种期望问题的时候,逆推显然是最好的选择,因为不满足情况的转移在之后的转移中会自动舍弃
Code
#include<bits/stdc++.h>
#define N 16
#define db double
using namespace std;
int k,n;
int v[N],tot[N],num;
double dp[105][(1<<N)],ans;
int main(){
cin>>k>>n;
int range=(1<<n)-1;
for(int i=0;i<n;i++){
int tmp;
scanf("%d%d",v+i,&tmp);
while(tmp){
tot[i]=tot[i]+(1<<(tmp-1));
scanf("%d",&tmp);
}
}
for(int i=k;i>=1;i--)
for(int j=0;j<=range;j++){
for(int l=0;l<n;l++){
if((tot[l]&j)==tot[l])
dp[i][j]+=max(dp[i+1][j],dp[i+1][j|(1<<l)]+v[l]);
else dp[i][j]+=dp[i+1][j];
}
dp[i][j]/=(db)n;
}
printf("%.6lf\n",dp[1][0]);
}
//g++ award.cpp -o test -std=c++14 -O2 -lm