洛谷链接:
思路:
我们用一个数对( i,j,k)来表示一个状态,i是r被吃个数,j是s被吃个数,k是p被吃个数。
每个状态都有一个对应的概率(本轮次取到这种情况的概率),比如(0,0,0)时,游戏进行了0轮,只有这一种状态,所以它概率是1,而第1轮有三个状态,(1,0,0),(0,1,0)和(0,0,1),他们概率总和是1,这三个概率可以通过(0,0,0)状态时各方剩余人数计算得到。
这样,我们就可以构建出一个三叉树结构,(i,j,k)的子结点有(i+1,j,k),(i,j+1,k)和(i,j,k+1)三个。树结构不断往下延伸,每个点的概率都可以计算出来。延伸到什么时候停止呢?当所有叶子节点都满足 i=r,j=s,k=p中的两个的时候停止,因为此时游戏就结束了。
那是不是说就可以用dfs解决啦?不行!因为这样会带来大量的重复计算,比如(1,0,0)和(0,1,0)都有(1,1,0)这个子结点,他们代表相同的状态,但是dfs计算的时候却是分开计算的,两个(1,1,0)点下面的子树就被重复计算了。所以我们应该把这些相同的点“接到一起”,所以要用到动态规划,把相同结点的结果【累加】到dp数组的同一个位置,这样就能避免重复计算啦。
具体细节见代码和注释。
代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 105;
double dp[maxn][maxn][maxn]; //dp[i][j][k]表示r,s,p分别被吃了i,j,k个的概率
double ansr=0, anss=0, ansp=0;
int r, s, p;
int main(){
cin >> r >> s >> p; //r吃s,s吃p,p吃r
dp[0][0][0] = 1.0;
for(int i=0; i<=r; i++){
for(int j=0; j<=s; j++){
for(int k=0; k<=p; k++){
int a=r-i, b=s-j, c=p-k; //r,s,p剩余人数
int tot = c*a+a*b+b*c; //概率分母
//当至少有两个种族存活的时候,可以进行递推(只剩一个种族,则递推必定停止)
//每一个状态都可能由多条路线递推过来,这些概率要累加,所以是+=
if(c&&a) dp[i+1][j][k] += dp[i][j][k]*(double)c*a/tot;
if(a&&b) dp[i][j+1][k] += dp[i][j][k]*(double)a*b/tot;
if(b&&c) dp[i][j][k+1] += dp[i][j][k]*(double)b*c/tot;
//只剩一个种族就记录结果(注意,有多种情况导致只剩这一种族,所以是+=)
if(!b&&!c) ansr += dp[i][j][k];
if(!a&&!c) anss += dp[i][j][k];
if(!a&&!b) ansp += dp[i][j][k];
}
}
}
printf("%.12f %.12f %.12f\n", ansr, anss, ansp);
}
update 2022/4/18
做法1:普通的概率dp
#include <bits/stdc++.h>
#define debug cout<<"!!!"<<endl;
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
#define ROF(i, a, b) for (int i = (a); i >= (b); i--)
#define pii pair<int,int>
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int N = 105;
int a,b,c;
double f[N][N][N],x,y,z;
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin>>a>>b>>c; f[a][b][c] = 1;
ROF(i,a,0) ROF(j,b,0) ROF(k,c,0){
//1.统计答案
if(j==0 && k==0 && i!=0) x += f[i][j][k];
if(i==0 && k==0 && j!=0) y += f[i][j][k];
if(i==0 && j==0 && k!=0) z += f[i][j][k];
//2.考虑递推
int tot = i*j+i*k+j*k;
if(tot==0) continue; //如果tot是0,那递推就没有意义
if(i>0) f[i-1][j][k] += f[i][j][k] * i*k/tot; //r被p吃
if(j>0) f[i][j-1][k] += f[i][j][k] * i*j/tot; //s被r吃
if(k>0) f[i][j][k-1] += f[i][j][k] * j*k/tot; //p被s吃
}
cout<<fixed<<setprecision(12);
cout<<x<<' '<<y<<' '<<z;
}
做法2:记忆化dfs式 概率dp
想法很容易理解,但是边界条件很多,写起来有些繁琐。
#include <bits/stdc++.h>
#define debug cout<<"!!!"<<endl;
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
#define ROF(i, a, b) for (int i = (a); i >= (b); i--)
#define pii pair<int,int>
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int N = 105;
int a,b,c;
double f[N][N][N],x,y,z;
double dfs(int i,int j,int k){
double& dp = f[i][j][k];
if(dp) return dp;
if(i<a && !(j==0 && k==0)) dp += dfs(i+1,j,k)*(i+1)*k/((i+1)*j+(i+1)*k+j*k);
if(j<b && !(i==0 && k==0)) dp += dfs(i,j+1,k)*(j+1)*i/((j+1)*i+(j+1)*k+i*k);
if(k<c && !(i==0 && j==0)) dp += dfs(i,j,k+1)*(k+1)*j/((k+1)*i+(k+1)*j+i*j);
return dp;
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin>>a>>b>>c; f[a][b][c] = 1;
FOR(i,1,a) x+=dfs(i,0,0);
FOR(j,1,b) y+=dfs(0,j,0);
FOR(k,1,c) z+=dfs(0,0,k);
cout<<fixed<<setprecision(12);
cout<<x<<' '<<y<<' '<<z;
}