專題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\)位之前的概率。這樣我們可以寫出狀態轉移方程。
\[f[i][j]= \begin{equation} \begin{cases} p_1f[i][j]+p_2f[i][i]+p_4 & j=1 \\ p_1f[i][j]+p_2f[i][j-1]+p_3f[i-1][j-1]+p_4 & 2\leq j\leq k \\ p_1f[i][j]+p_2f[i][j-1]+p_3f[i-1][j-1] & k<j\leq i \end{cases} \end{equation} \]和上一題出現了類似的情況在\(j=1\)的情況下,\(f[i][1]\)與\(f[i][i]\)都是未知,因此公式需要進行處理。
首先將\(f[i][j]\)合併同類項,得到:
\[f[i][j]= \begin{equation} \begin{cases} \frac{p_2f[i][i]}{1-p_1}+\frac{p_4}{1-p_1} & j=1 \\ \frac{p_2f[i][i]}{1-p_1}+\frac{p_3f[i-1][j-1]}{1-p_1}+\frac{p_4}{1-p_1} & 2\leq j\leq k \\ \frac{p_2f[i][i]}{1-p_1}+\frac{p_3f[i-1][j-1]}{1-p_1} & k<j\leq i \end{cases} \end{equation} \]令\(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]\)看作常數:
\[c[i]= \begin{equation} \begin{cases} k_4&j=1 \\ k_3f[i-1][j-1]+k_4&2\leq j \leq k \\ k_3f[i-1][j-1]&k<j\leq i \end{cases} \end{equation} \]由此公式可以列出:
\[\begin{equation} \begin{cases} f[i][1]=k_2f[i][i]+c[1] \\ f[i][2]=k_2f[i][1]+c[2] \\ ...\\ f[i][i]=k_2f[i][i-1]+c[i] \end{cases} \end{equation} \]現在,我們可以通過反覆代入求出\(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';
}