专题6 - 概率dp
概率dp
在比赛中会有很多题目涉及求期望或概率的题目,虽然用数学办法可能可以算出结果,但通过dp的方式求得概率或期望才是出题人所希望的。
POJ2096 Collecting Bugs
一个倒霉蛋每天都能收集到bug,一个软件有\(s\)个子系统,会产生\(n\)种不同的bug,每个bug属于某个子系统的概率是\(1/s\),属于某种分类的概率为\(1/n\),求发现\(n\)种bug且每个子系统都发现bug的天数的期望。
如果通过正常的数学方法计算,会发现这是一个无穷级数,计算会相对繁琐。
不过不难发现,当已经发现了\(n\)种bug,且每个子系统都发现bug时期望为\(0\),而且可以通过此来推出之前状态的期望。
我们用\(f[i][j]\)来表示已经发现\(i\)种bug,且\(j\)个子系统发现了bug的情况下剩余天数的期望。那么不难发现\(f[n][s]=0\)。
现在有以下四种情况:
-
发现了新的bug种类,且为新的子系统,此时\(f[i][j]+=(1-i/s)\times(1-j/n)\times(f[i+1][j+1]+1)\)
-
发现了新的bug种类,但为重复的子系统,此时\(f[i][j]+=(1-i/s)\times j/n\times(f[i+1][j]+1)\)
-
发现了重复的bug种类,但为新的子系统,此时\(f[i][j]+=i/s\times(1-j/n)\times(f[i][j+1]+1)\)
-
发现了重复的bug种类,且为重复的子系统,此时\(f[i][j]+=i/s\times j/n\times(f[i][j]+1)\)
最终,\(f[i][j]\)就等于上述式子相加,将\(f[i][j]\)合并就能够进行递推了。
#include<iostream>
#include<algorithm>
#include<string.h>
#include<iomanip>
#define ll long long
#define pb push_back
#define fast ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
const int maxn = 1010;
double f[maxn][maxn];
int main()
{
fast;
int n,s;
cin>>n>>s;
memset(f,0,sizeof(f));
f[n][s]=0;
for(int i=n;i>=0;i--)
{
for(int j=s;j>=0;j--)
{
if(i==n && j==s) continue;
f[i][j]=1.0*i/n*(s-j)/s*f[i][j+1]+1.0*(n-i)/n*j/s*f[i+1][j]+1.0*(n-i)/n*(s-j)/s*f[i+1][j+1]+1.0;
f[i][j]=f[i][j]*(1.0*n*s/(n*s-i*j));
}
}
// for(int i=0;i<=n;i++)
// {
// for(int j=0;j<=s;j++)
// {
// cout<<f[i][j]<<' ';
// }
// cout<<'\n';
// }
cout<<fixed<<setprecision(4)<<f[0][0]<<'\n';
}
NC210477 带富翁
小明在玩一款带富翁游戏,这个游戏具体来说就是有\(n\)个奖励点,每个奖励点有一定的奖励分。一开始他站在位置\(1\)。每次他都会扔一个有666面的筛子,如果扔到了\(x\),并且小明现在站在\(i\)这个位置,小明就会向前进\(x\)步到达\(i+x\)这个位置。如果出现了下一步会超出\(n\)的情况,必须重新投掷。到达\(n\)即视为结束,问得分的期望为多少。
根据上一题的思路,我们可以用\(f[i]\)来表示在位置\(i\)时之后能获得分数的期望。
- 当\(i \leq n-6\)时,\(f[i]=a[i]+\sum\limits_{j=i+1}^{i+6}(f[j]/6)\)
- 当\(i>n-6\)时,\(f[i]=a[i]+(f[i+1]+...+f[n])/(n-i)\)
#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define fast ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
const int maxn = 110;
int a[maxn];
double f[maxn]={0};
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
f[n]=a[n];
for(int i=n-1;i>=1;i--)
{
f[i]=a[i];
int num=min(i+6,n)-i;
for(int j=i+1;j<=min(i+6,n);j++)
{
f[i]+=f[j]/num;
}
}
cout<<fixed<<setprecision(7)<<f[1]<<'\n';
}
NC210481 筛子游戏
题目描述不过多赘述,具体看链接。
首先我们可以通过三重循环将所有可能的和的概率统计出来,这里用\(p[k]\)来表示分数加上\(k\)的概率。另外,分数归零用\(p[0]\)表示。
用\(f[i]\)来表示当前分数为\(i\)时需要次数的期望。可以比较容易地得出\(f[i]=\sum\limits_{k=3}^{18}f[i+k]\times p[k]+f[0]\times p[0]+1\)。
但这个式子并不能直接进行递推,因为每个式子中都包含\(f[0]\),而\(f[0]\)恰好是我需要的结果。
如果每个结果都与\(f[0]\)有关,那我们不妨设\(f[i]=A[i]\times f[0]+B[i]\)。此时\(A[n]=0,B[n]=0\)。
代入\(f[i]\)原式可以得到:
\(A[i]=\sum\limits_{k=3}^{18}A[i+k]\times p[k]+p[0]\)
\(B[i]=\sum\limits_{k=3}^{18}B[i+k]\times p[k]+1\)
由此递推式,我们可以求得\(A[i]\)和\(B[i]\),而\(f[0]=B[0]/(1-A[0])\),这样就可以求得答案了。
#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define fast ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
const int maxn = 510;
int n;
int k1,k2,k3;
double p[50]={0};
double f[maxn]={0};
double A[maxn]={0};
double B[maxn]={0};
int a,b,c;
int main()
{
cin>>n>>k1>>k2>>k3>>a>>b>>c;
for(int i=1;i<=k1;i++)
{
for(int j=1;j<=k2;j++)
{
for(int m=1;m<=k3;m++)
{
if(i==a && j==b && m==c) continue;
p[i+j+m]++;
}
}
}
for(int i=1;i<=18;i++)
{
p[i]=p[i]/(k1*k2*k3);
}
p[0]=1.0/(k1*k2*k3);
for(int i=n;i>=0;i--)
{
for(int j=3;j<=(k1+k2+k3);j++)
{
A[i]+=p[j]*A[i+j];
B[i]+=p[j]*B[i+j];
}
A[i]+=p[0];
B[i]+=1;
}
f[0]=B[0]/(1-A[0]);
cout<<fixed<<setprecision(7)<<f[0]<<'\n';
}
NC210487 食堂
我们可以用\(f[i][j]\)来表示队伍中有\(i\)个人,吉吉国王处于前\(j\)个位置处时,在关门前排在\(k\)位之前的概率。这样我们可以写出状态转移方程。
和上一题出现了类似的情况在\(j=1\)的情况下,\(f[i][1]\)与\(f[i][i]\)都是未知,因此公式需要进行处理。
首先将\(f[i][j]\)合并同类项,得到:
令\(k_2,k_3,k_4\)分别等于\(\frac{p_2}{1-p_1},\frac{p_3}{1-p_1},\frac{p_4}{1-p_1}\),然后我们可以表示出所有的常数项(在这里将\(f[i-1][j-1]\)看作常数:
由此公式可以列出:
现在,我们可以通过反复代入求出\(f[i][i]\),进而求出\(f[i][1]\),然后就能够递推出所有情况了。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2010;
int n,m,k;
double p1,p2,p3,p4;
double f[maxn][maxn]={0};
double p[maxn];
double c[maxn];
int main()
{
cin>>n>>m>>k;
cin>>p1>>p2>>p3>>p4;
double k2=p2/(1-p1);
double k3=p3/(1-p1);
double k4=p4/(1-p1);
f[1][1]=p4/(1-p1-p2);
p[0]=1;
for(int i=1;i<=n;i++)
{
p[i]=p[i-1]*k2;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
if(j<=k) c[j]=f[i-1][j-1]*k3+k4;
else c[j]=f[i-1][j-1]*k3;
}
double tmp=0;
for(int j=1;j<=i;j++)
{
tmp+=p[i-j]*c[j];
}
f[i][i]=tmp/(1-p[i]);
f[i][1]=k2*f[i][i]+k4;
for(int j=2;j<i;j++)
{
f[i][j]=k2*f[i][j-1]+c[j];
}
}
cout<<fixed<<setprecision(5)<<f[n][m]<<'\n';
}