题意:
Rainbow 把一副扑克牌(54张)随机洗开,倒扣着放成一摞。然后 Admin 从上往下依次翻开每张牌,每翻开一张黑桃、红桃、梅花或者方块,就把它放到对应花色的堆里去。Rainbow 想问问 Admin,得到 A 张黑桃、B 张红桃、C 张梅花、D 张方块需要翻开的牌的张数的期望值 E 是多少?特殊地,如果翻开的牌是大王或者小王,Admin 将会把它作为某种花色的牌放入对应堆中,使得放入之后 E 的值尽可能小。
分析:设f[x][y][z][w][p][q],表示已经翻开了x张黑桃,y张红桃,z张梅花,w张方块,并且小王的状态为p,大王的状态为q时的期望值。p=0代表没有抽到小王,p=1、2、3、4,分别代表将小王视作黑桃、红桃、梅花、方块。大王的记录方法同理。
当前已经翻开的牌的总数为sum=x+y+z+w+(p!=0)+(q!=0),那么剩余牌的数目就是54-sum,那么下次抽取到的牌为黑桃的概率就是(13-x)/(54-sum),其余花色同理,如果大王或者小王当前还没有被抽到,那么下次被抽到的概率就是1/(54-sum),注意要对大小王进行分类讨论,使得当大小王放入某花色堆时总期望尽量小,递推公式不大好打,我写到了纸上:
待求状态为f[0][0][0][0][0][0],直接利用记忆化搜索进行求解就好。
在数学期望递推、数学期望动态规划中,我们通常把终止状态(翻开的牌数达到要求)作为初值,把起始状态(尚未翻开任何牌)作为目标,倒着进行计算。这是因为在很多情况下,起始状态是唯一的(0,0,0,0,0,0),而终止状态却有很多(只要各花色翻开的牌数都足够即可)。
下面是代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<queue>
using namespace std;
#define INF 1e20
const int N= 15;
double f[N][N][N][N][5][5];
int A,B,C,D;
double dp(int x,int y,int z,int w,int p,int q)
{
//a,b,c,d分别为当前已经翻开的黑桃,红桃,梅花,方块的张数
int a=x+(p==1)+(q==1);
int b=y+(p==2)+(q==2);
int c=z+(p==3)+(q==3);
int d=w+(p==4)+(q==4);
if(f[x][y][z][w][p][q]) return f[x][y][z][w][p][q];
if(a>=A&&b>=B&&c>=C&&d>=D) return 0;
int sum=54-a-b-c-d;
double ans=0;
if(x<13) ans+=1.0*(13-x)/sum*(dp(x+1,y,z,w,p,q)+1);
if(y<13) ans+=1.0*(13-y)/sum*(dp(x,y+1,z,w,p,q)+1);
if(z<13) ans+=1.0*(13-z)/sum*(dp(x,y,z+1,w,p,q)+1);
if(w<13) ans+=1.0*(13-w)/sum*(dp(x,y,z,w+1,p,q)+1);
if(p==0)//小王放置各堆中的期望最小值
{
double t=INF;
for(int i=1;i<=4;i++)
t=min(t,1.0/sum*(dp(x,y,z,w,i,q)+1));
ans+=t;
}
if(q==0)//大王放置各堆中的期望最小值
{
double t=INF;
for(int i=1;i<=4;i++)
t=min(t,1.0/sum*(dp(x,y,z,w,p,i)+1));
ans+=t;
}
return f[x][y][z][w][p][q]=ans;
}
int main()
{
scanf("%d%d%d%d",&A,&B,&C,&D);
int t=0;
if(A>13) t+=A-13;
if(B>13) t+=B-13;
if(C>13) t+=C-13;
if(D>13) t+=D-13;
if(t>2) puts("-1.000");
else printf("%.3lf",dp(0,0,0,0,0,0));
return 0;
}