题意:
一个游戏如果不出意外会花 $n(<=5000)$个时间完成,世界纪录是$r(<=5000)$,有$m(<=50)$种可能发生的意外情况浪费时间。
意外情况格式如下:
$t(<n)$ $p(0<p<1)$ $d$($<=1000$) 三个数,表示该事件发生在正常流程下的第$t$秒时,发生概率为$p$,浪费$d$的时间。
你可以随时重新开始游戏,问从一开始玩到成功打破记录(一局游戏从开始到结束时间小于$r$)期望一共花多少时间。
首先按照期望类题的套路,倒推状态转移,我们设$F[i][j]$为进行到正常流程下第$i$秒,实际本次尝试已经花了$j$秒还期望花多少时间突破记录。显然,答案就是$F[0][0]$
下面就是写转移方程,由于意外事件很少,我们将$F[i][j]$的意义改为已经进行到了第$i$个意外时间刚开始的时间,本次尝试已经花费了$j$秒且不知道这次事件是否会发生的期望剩余时间。得到如下转移方程:
$F[i][j]=(1-p[i])*(F[i+1][j+t[i+1]-t[i]]+t[i+1]-t[i])+p[i]*min(F[0][0],F[i+1][j+d[i]+t[i+1]-t[i]]+d[i]+t[i+1]-t[i]) (j+d[i]+n-t[i]<r)$
$F[i][j]=(1-p[i])*(F[i+1][j+t[i+1]-t[i]]+t[i+1]-t[i])+p[i]*F[0][0] (j+d[i]+n-t[i]>=r)$
但是我们发现我们的转移方程出现了$F[0][0]$,而$F[0][0]$又是我们的最终答案,这就陷入了一个死环。当时训练的时候想到这一步就懵了,以为最后是要解方程,但是$min$又限制了我们无法通过解方程来得到答案。
看了题解之后才顿悟,既然$F[0][0]$未知,我们能不能尝试去二分$F[0][0]$,转移是用二分值,之后通过比较它与算出的$F[0][0]$的大小确定该调大还是调小。如果二分值大于$F[0][0]$,就意味着在一些决策点我们本应该选择重来但是我们选择了继续打,所以应当将二分值调小;如果小于$F[0][0]$,就意味着一些本应该继续打的地方我们选择了重来,所以应当将二分值调大。
1 #include<iostream> 2 #include<cstdlib> 3 #include<cstdio> 4 #include<cstring> 5 #include<cmath> 6 #include<algorithm> 7 #define N 5005 8 #define double long double 9 using namespace std; 10 int n,r,m; 11 int T[N]; 12 double P[N]; 13 int D[N];; 14 double F[55][N]; 15 bool check(double X) 16 { 17 for(int i=m;i>=0;i--) 18 { 19 for(int j=T[i];j<r-(n-T[i]);j++) 20 { 21 F[i][j]=P[i]*(T[i+1]-T[i]+F[i+1][j+T[i+1]-T[i]]); 22 if(j+D[i]+(n-T[i])<r) 23 { 24 F[i][j]+=(1.0-P[i])*min(X,D[i]+T[i+1]-T[i]+F[i+1][D[i]+T[i+1]-T[i]+j]); 25 } 26 else 27 { 28 F[i][j]+=(1.0-P[i])*X; 29 } 30 } 31 } 32 return X>F[0][0]; 33 } 34 int main() 35 { 36 scanf("%d%d%d",&n,&r,&m); 37 for(int i=1;i<=m;i++) 38 { 39 scanf("%d%Lf%d",&T[i],&P[i],&D[i]); 40 } 41 T[m+1]=n; 42 P[0]=1; 43 double li=0,ri=1e17,mid,ans; 44 while(fabs(ri-li)>=1e-8) 45 { 46 mid=(li+ri)/2.0; 47 if(check(mid)) ri=mid-(1e-8),ans=mid; 48 else li=mid+(1e-8); 49 } 50 printf("%.10Lf ",ans); 51 return 0; 52 } 53