• 【概率期望动态规划】


    虽然概率DP有许多数学期望的知识,但是终究无法偏离动态规划的主题。动态规划该有的特点继续保留,另外增添了一些概率期望的神秘色彩。

    1~8题出处:hdu4576   poj2096   zoj3329   poj3744   hdu4089   hdu4035   hdu4405   hdu4418

    ·跟随例题慢慢理解这类问题……Smile with tongue out

    [1]机器人

    ·述题意:

         多组输入n,m,l,r。表示在一个环上有n个格子。接下来输入m个w表示连续的一段命令,每个w表示机器人沿顺时针或者逆时针方向前进w格,已知机器人是从1号点出发的,输出最后机器人停在环上[l,r]区间的概率。n(1≤n≤200) ,m(0≤m≤1,000,000)。

    ·分析:

         这是一道求概率的题吗?是的。我们可以想象机器人从1点开始,每次分身前往距离为wi的两点,最后呢就会有很多很多分身,落得到处都是,然后呢统计在[l,r]的分身个数,再除以总个数就是概率呀……

         其实这类问题正是这样做的——计算出每种情况占种情况的概率,然后回答问题。不过呢为了统一格式,所以在网上见到解法,都是机器人一分为二变成两个0.5机器人而不是变成两个和原来一样的机器人。总结而言,0.5机器人就是概率的体现。

        如果我们使用f[i]表示i这个位置会出现多少个机器人分身,那么机器人所在点是这样为周围贡献答案的:

                  image

              经历了上述美妙的形象化理解后,这道题的状态转移就很明显了:

    ①刷表法:  f[i-w]+=f[i]*0.5 , f[i+w]+=f[i]*0.5

    ②填表法:  f[i]=f[i-w]*0.5+f[i+w]*0.5

              最后一个小提醒是,由于这道题是环形问题,所以呢如果超出了范围,可以进行取模或者特判来维持正确的转移。

        代码在这里:

     1 #include<stdio.h>
     2 #include<cstring>
     3 #define go(i,a,b) for(int i=a;i<=b;i++)
     4 using namespace std;const int N=500;
     5 int n,m,l,r,w,cur;
     6 double f[2][N],ans;
     7 int main()
     8 {
     9     while(scanf("%d%d%d%d",&n,&m,&l,&r),m+n+l+r)
    10     {
    11         memset(f,0,sizeof(f));
    12         f[cur=ans=0][1]=1;
    13         go(j,1,m)
    14         {
    15             scanf("%d",&w);w%=n;cur^=1;
    16             go(i,1,n)f[cur][i]
    17             =f[cur^1][i+w>n?i+w-n:i+w]/2
    18             +f[cur^1][i-w<1?i-w+n:i-w]/2;    
    19         }
    20         go(i,l,r)ans+=f[cur][i];
    21         printf("%.4f
    ",ans);
    22     }
    23     return 0;
    24 }//Paul_Guderian

    [2]收集漏洞

    ·述题意:

         输入n,s表示这里存在n种漏洞和s个系统(0<n,s<=1000)。工程师可以花费一天去找出一个漏洞——这个漏洞可以是以前出现过的种类,也可能是未曾出现过的种类,同时,这个漏洞出现在每个系统的概率相同。要求得出找到n种漏洞,并且在每个系统中均发现漏洞的期望天数。

    ·分析:

         这是一道求期望值的题目。题目中的两个关键字提醒我们二维状态设计或许很美妙。根据上题的路子,我们用状态f[i][j]表示已经发现了i种漏洞同时已经有j个系统发现了漏洞的情况下最终达到题目要求(f[n][s])的期望天数。

         进一步。由题目可知,其实每次漏洞有两种情况(发现过的漏洞和新的漏洞),同时这个漏洞所在的系统也有两种情况(之前已经发现漏洞的系统和之前没有发现漏洞的系统),所以组合一下,共有四情况,一起来转移吧:

                   image 

           由图,我们可以轻松得到转移方程吗?还差一丢丢。因为目的是求出期望值——什么是期望值?好吧,暂时可以理解为“权值 x 概率”。因此期望Dp的转移是有代价的,而不像概率Dp那样简单统计了。另外一个问题,类似于上文的机器人分身,当前状态的期望值有多个转移方向,所以此处要乘上概率——也就是选择这一步的概率P,如下:

         f[i][j]—>f[i+1][j+1]: P1=(n-i)*(s-j)/n*s

         f[i][j]—>f[i+1][j]   : P2=(n-i)*j    /n*s

         f[i][j]—>f[i][j+1]   : P3=i*(s-j)    /n*s

         f[i][j]—>f[i][j]      : P4=i*j        /n*s

         然后算上转移的代价(1天),我们开始思考最终的DP转移方程式。这里我们将f[n][s]=0定为边界——很合理,表示找到n种漏洞,有s个系统发现漏洞距离目标状态的期望天数(就是一样的状态,所以期望天数是0啊)。据此我们设计出一个逆推的Dp方程式:

                                        f[i][j]=

       (f[i][j]+1)*P4+(f[i][j+1]+1)*P3+(f[i+1][j]+1)*P2+(f[i+1][j+1]+1)*P1

         你会发现方程左右两边都有f[i][j],所以就对式子进行化简。化简如下:

    f[i][j]=f[i][j]*P4+f[i][j+1]*P3+f[i+1][j]*P2+f[i+1][j+1]*P1+(P1+P2+P3+P4)

    f[i][j]*(1-P4)  = f[i][j+1]*P3+f[i+1][j]*P2+f[i+1][j+1]*P1 +      1

         最终就是将左边系数除过去然后带入p1p2p3p4,逆推转移就是了,答案当然就在f[0][0]诞生啦,代码也来啦:

     1 #include<stdio.h>
     2 #define ro(i,a,b) for(int i=a;i>=b;i--)
     3 const int N=1003;int n,m;double f[N][N];
     4 int main()
     5 {
     6     while(~scanf("%d%d",&n,&m))
     7     {
     8         f[n][m]=0;
     9         ro(i,n,0)
    10         ro(j,m,0)if(i!=n||j!=m)
    11         f[i][j]=
    12         (
    13             f[i+1][j]*(n-i)*j+
    14             f[i][j+1]*i*(m-j)+
    15             f[i+1][j+1]*(n-i)*(m-j)+n*m
    16         )/
    17         (
    18             n*m-i*j
    19         );        
    20         printf("%.4f
    ",f[0][0]);
    21     }
    22     return 0;
    23 }//Paul_Guderian

          一个补充问题:为什么期望dp常常逆推?大米饼认为期望DP中状态转移各个去向的概率决定了这一点。如果要求解,我们必须要知道转移去向的概率是多少(就像上文发现漏洞的四种情况具有不同的概率一样),也就相当于机器人分身。那么逆推情况下,各个来源的概率正是实际问题中的概率(比如漏洞是新的且在新系统就是(n-i)*(s-j)/n*s)。如果顺推,由于一些来源状态无法到达或者无实际意义,很多时候转移的概率并不是实际问题的概率。更加浅显易懂地说就是:逆推的概率符合实际,顺推的概率只是形式上的(即填表法得出刷表法),不一定符合实际。

    [3]一个人的游戏

    ·述大意:

           有三个骰子,分别有k1,k2,k3个面,初始分数是0。第i骰子上的分数从1道ki。当掷三个骰子的点数分别为a,b,c的时候,分数清零,否则分数加上三个骰子的点数和,当分数>n的时候结束。求需要掷骰子的次数的期望。

    (0<=n<= 500,1<K1,K2,K3<=6,1<=a<=K1,1<=b<=K2,1<=c<=K3)

    ·分析:

          这是一道求期望的题。首先总体感悟一下可以知道状态有两类转移途径,分别是加分数和清空分数。还是像以前一样,我们定义f[i]表示当前分数为i的时候,到达大于等于n分数的状态的期望次数。对于清空情况的概率我们使用P0表示。

          首先,由于我们已知三个骰子可能的点数,那么我们可以算出所有可能分数的概率,即用p[i]表示三个骰子加起来分数为i的概率。

          上文的处理使得DP方程式很容易写出来:

        image

                然后就轻轻地写出DP方程式(注意,还是逆推):

         f[i] = f[0]*P0 + (f[i+k]*p[i+k])  + 1

         看上去问题已经解决,但是出现了一个很大的问题:逆推是从大的i循环至小的i,但是现在每个式子都含有一个f[0],这样就没有办法转移状态了(似乎形成了一个环,然后在其中迷失自我) 

         怎么办啊?啊啊啊,完了完了。

         还没完!既然f[0]违背常理,我们不能立刻求出来,那么就将它作为未知数好了。首先我们找出每个方程式的统一格式,可以写成这样:

          f[i] = f[0]*ai+bi (原因是每个式子都含有f[0])————①

          那么对于上面的方程式,其中的f[i+k]就可以被拆成:

          f[i+k] = f[0]*ai+k+bi+k

          然后带入原来的式子得出: 

          原式:f[i]  = f[0]*P0 + (f[i+k]*p[i+k])  + 1

                f[i]  = f[0]*P0 + ((f[0]*ai+k+bi+k)*p[i+k]) +1————②

          然后我们试图将这个式子掰成和①式相同的形式:

              ②式:f[i]  = f[0]*(P0+ai+k*p[i+k]) + (bi+k*p[i+k]) + 1

              ①式:f[i] =  f[0]*         ai             +           bi

          因此,你的方法奏效了,因为你得到了重要的式子:

               ai=P0+ai+k*p[i+k]

               bi=(bi+k*p[i+k])+1

           在逆推的条件下,ai,bi均可以被递推出来,就替代了原来f[]递推的职责,使得我们顺利走到f[0]=f[0]*a0+b0从而推出:f[0]=b0/(1-a0)——我们梦寐以求的答案。

     1 #include<stdio.h>
     2 #include<algorithm>
     3 #include<iostream>
     4 #include<math.h>
     5 #include<cstring>
     6 #define go(i,a,b) for(int i=a;i<=b;i++)
     7 #define ro(i,a,b) for(int i=a;i>=b;i--)
     8 #define fo(i,a,x) for(int i=a[x],v=e[i].v;i;i=e[i].next,v=e[i].v)
     9 #define mem(a) memset(a,0,sizeof(a))
    10 using namespace std;
    11 const int N=700;
    12 int T,n,K1,K2,K3,A,B,C,sum;
    13 double p[N],P,x[N],y[N];
    14 int main()
    15 {
    16     scanf("%d",&T);
    17     while(T--&&scanf("%d%d%d%d%d%d%d",&n,&K1,&K2,&K3,&A,&B,&C))
    18     {
    19         mem(p),mem(x),mem(y);
    20         sum=K1+K2+K3;P=1.0/K1/K2/K3;
    21         go(a,1,K1)go(b,1,K2)go(c,1,K3)
    22         if(a!=A||b!=B||c!=C)p[a+b+c]+=P;
    23         ro(i,n,0)
    24         {
    25             x[i]=P,y[i]=1;
    26             go(k,3,sum)
    27             {
    28                 x[i]+=p[k]*x[i+k],
    29                 y[i]+=p[k]*y[i+k];    
    30             }
    31         }
    32         printf("%.15lf
    ",y[0]/(1-x[0]));
    33     }
    34     return 0;
    35 }//Paul_Guderian

        总结来说,这道题相当于建立了一个方程组,然后解题的过程就是解方程的过程,这类题型在期望DP中十分常见。当然,这道题由于只有f[0]违反了逆推顺序,所以可以简单地处理系数来解出f[0]。但是,还有一些题是相互制约、环环相扣的局面,到那时候只有高斯消元才能拯救局面了。

    [4]YYF侦查员

    ·述大意:

            输入n表示共有n个地雷(0<n<=10),并且输入每个地雷所在的位置ai(ai为不大于108的正整数)。现在求从1号位置出发越过所有地雷的概率。用两种行走方式:①走一步②走两步(不会踩爆中间那个雷)。这两个行为的概率分别为p和(1-p)。

    ·分析:

          怎样才叫不被炸飞呢?那就是不踩任何地雷。可是怎么写转移方程式才能满足这个条件呢?由于同时满足所有地雷都不踩较为困难,所以尝试分步。

         插播一句,无论在何时何地,DP方程式还是很容易浮现脑海的:

         令f[i]表示走到i位置还活着的概率:

         f[i]=f[i-1]*p+f[i-2]*(1-p)

         我们根据雷的位置将数轴分为n+1各部分,那么在雷之间全是安全美丽的土地,可以尽情行走——到了雷边儿上,就要注意了,一定要尝试跨过那个讨厌的雷:

    image

          我们发现,如果当前位置位于i,那么只能走i+2才能幸存。对于相邻两个雷(设他们的位置分别为l,r(l<r))之间漫长的区域,其实我们只需要算出从l+1开始走,并且到达r的概率(表示人成功越过l位置的雷,然后在r位置被成功丧命),然后呢1减去这个概率,正是这个人在这一段区间存活的概率。

          上述处理方式总是感觉是要统一每个区间(两个雷之间的区域)的概率计算方式,为什么呢?首先,最终答案就是各个区间的存活概率相乘的结果,很方便但是这不是这样做的主要原因。真正的原因是,让我们留意一下数据范围,跨越雷区最远会行走108,如果直接一个位置一个位置进行状态转移,就会慢,然后就TLE。分段处理到底可以干嘛呢?

          注意上面那图中的"随便走,愉快…",说明在空旷的无雷地带上DP方程式做着形式千篇一律的状态转移,怎么加速?很明显就可以想到矩阵幂

          所以最终的做法就是,对于每个区间算出在该区间内在区间左端点雷炸死人的概率,然后相乘得到答案,其中每一段内的状态转移使用矩阵幂维护。

     1 #include<stdio.h>
     2 #include<algorithm>
     3 #define go(i,a,b) for(int i=a;i<=b;i++)
     4 const int N=15;
     5 int n,a[N];double p,ans;
     6 struct Mat
     7 {
     8     double mat[3][3];
     9     void init1()
    10     {
    11         mat[1][1]=p,mat[1][2]=1-p;a[0]=0;
    12         mat[2][1]=1,mat[2][2]=0;ans=1;
    13     }
    14     void init2()
    15     {
    16         mat[1][1]=mat[2][2]=1;
    17         mat[1][2]=mat[2][1]=0;
    18     }
    19 }t;
    20 void Mul(Mat &T,Mat g)
    21 {
    22     Mat res;
    23     go(i,1,2)go(j,1,2){res.mat[i][j]=0;
    24     go(k,1,2)res.mat[i][j]+=T.mat[i][k]*g.mat[k][j];}T=res;
    25 }
    26 void Pow(Mat T,int x)
    27 {
    28     Mat res;res.init2();
    29     while(x){if(x&1)Mul(res,T);Mul(T,T);x>>=1;}    
    30     ans*=(1-res.mat[1][1]);
    31 }
    32 int main()
    33 {
    34     while(~scanf("%d%lf",&n,&p))
    35     {
    36         go(i,1,n)scanf("%d",a+i);std::sort(a+1,a+n+1);t.init1();
    37         go(i,1,n)if(a[i]!=a[i-1])Pow(t,a[i]-a[i-1]-1);
    38         printf("%.7f
    ",ans);
    39     }
    40     return 0;
    41 }//Paul_Guderian

    [5]账号激活

    ·述大意:

         输入n,m表示一款注册账号时,小明现在在队伍中的第m个位置有n个用户在排队。每处理一个用户的信息时(指处在队首的用户),可能会出现下面四种情况:

    1.处理失败,重新处理,处理信息仍然在队头,发生的概率为p1;

    2.处理错误,处理信息到队尾重新排队,发生的概率为p2;

    3.处理成功,队头信息处理成功,出队,发生的概率为p3;

    4.服务器故障,队伍中所有信息丢失,发生的概率为p4;

          问当他前面的信息条数不超过k-1同时服务器故障的概率。(1<=n,m<=2000)

    ·分析

         这是一道概率DP。首先根据题目给出的"位置""用户数"两个关键字可以先试着写出状态:f[i][j]表示当前队列里有i个人,然后小明排在第j位的时候达到目标状态的概率。

          这个定义很明显与上文的概率DP定义有所不同,因为这看上去有点像期望DP——到达某个状态的概率,而不是这个状态出现的概率。这样做的原因是答案在一个区间里(见题目)所以只要在这个区间里的,我们转移的时候就加上概率,如果不在这个区间里,那么很明显是不会贡献新的概率的。

          然后尝试写写转移方程式:

    image

            注意式子建立转移关系的原则是去掉不可能的情况(比如说小明激活成功了!),这个是不会影响概率的。然后呢,由于方程两边有相同的状态,所以像往常一样移项化简,得到对应的三个式子:
           j==1:f[i][1]=f[i][i]*P2/(1-P1)+P4/(1-P1)

       1<j<k+1:f[i][j]=f[i][j-1]*P2/(1-P1)+f[i-1][j-1]*P3/(1-P1)+P4/(1-P1)

             k<j:f[i][j]=f[i][j-1]*P2/(1-P1)+f[i-1][j-1]*P3/(1-P1)

        为了方便观察,我们换元使用新的系数:

            令:p1=P2/(1-P1),p2=P3/(1-P1),p3=P4(1-P1)

        原式进一步美妙起来:

           j==1:f[i][j]=f[i][i]*p1+p3—————————————①

       1<j<k+1:f[i][j]=f[i][j-1]*p1+f[i-1][j-1]*p2+p3 ————②

             k<j:f[i][j]=f[i][j-1]*p1+f[i-1][j-1]*p2 ——————③

         现在考虑按照什么顺序怎样递推?由于1式子的存在,好像转移关系又形成了一个环。除了调皮的1式外,2,3式子都严格遵循下标小推出下标大的状态的原则,因此,仅仅一个1式子违背常理,还是很好处理的:

         固定i,只动j。由于是i,j嵌套循环(令i在外层循环),那么对于f[i][]转移,根据从小到大的转移顺序,f[i-1][]的内容已经处理好了,也就是说可以看做常数,唯一不确定的(如式子2,3)就是f[i][j-1]。

         拿2式子入手:首先把常数项都塞成一坨,称作hahai,得到式子:
         f[i][j]=f[i][j-1]+hahai

         那么对于变幻的j,我们先看j取值区间为[1,i]的情况,则有:

         f[i][1]=f[i][i]*p1+p3(式子1)

         f[i][2]=f[i][1]*p1+haha2

         f[i][3]=f[i][2]*p1+haha3

                    ……

         f[i][i]=f[i][i-1]*p1+hahai

         然后呢就将每个式子带入下一个式子最终可以得到一个关于f[i][i]的可解方程。这里就是常数项的相加和乘p1的操作,所以累加一下记录就可以了。所以我们得到了f[i][i]的值,再根据方程式推出其他的值就很容易了。

         为什么这样做呢?因为我们发现f[i][i]是扰乱秩序的那个,所以我们想办法先得到它的值,从而恢复正常的地推顺序。

         总结地说,整个计算过程就是维护带入后的累加的值,和每个haha的和,最后就像普通的DP一样完美解决问题。

     1 #include<stdio.h>
     2 #define go(i,a,b) for(int i=a;i<=b;i++)
     3 const int N=2003;int n,m,k,_;
     4 double P[5],p1,p2,p3,f[2][N],B[N],sum,p_;
     5 int main()
     6 {
     7     while(~scanf("%d%d%d%lf%lf%lf%lf",&n,&m,&k,P+1,P+2,P+3,P+4))
     8     {
     9         if(P[4]<1e-9){puts("0.00000");continue;}
    10         p1=P[2]/(1-P[1]); 
    11         p2=P[3]/(1-P[1]); 
    12         p3=P[4]/(1-P[1]);
    13         f[_=0][1]=P[4]/(1-P[1]-P[2]);
    14         go(i,2,n)
    15         {
    16             sum=0;p_=1;
    17             go(j,1,k)B[j]=p2*f[_][j-1]+p3;
    18             go(j,k+1,i)B[j]=p2*f[_][j-1];
    19             go(j,1,i)sum=sum*p1+B[j],p_*=p1;
    20             _^=1;f[_][1]=p1*sum/(1-p_)+p3;
    21             go(j,2,i)f[_][j]=p1*f[_][j-1]+B[j];
    22         }    
    23         printf("%.5f
    ",f[_][m]);
    24     }
    25     return 0;
    26 }//Paul_Guderian

    [6]迷宫

    ·述大意:

          有n个房间,由n-1条隧道连通起来,形成一棵树,从结点1出发,开始走,在每个结点i都有3种可能(概率之和为1):1.被杀死,回到结点1处(概率为ki)2.找到出口,走出迷宫 (概率为ei)
    3.和该点相连有m条边,随机走一条求:走出迷宫所要走的边数的期望值。(2≤n≤10000)

    ·分析:
         这是一道求期望的题。如果设back[u],end[u]表示在节点u返回起点和走出迷宫的概率(哎呀,就是输入的数据),令m表示与点的节点个数,那么一个点走向每个儿子节点的概率为:(1-back[u]-end[u])/m

         根据上文信息,可以写出DP方程式(1为根节点):

         令f[u]表示在节点u通关的所需的边数期望,v与u相连。

         f[u]=f[1]*back[u]+end[u]*0+(1-back[u]-end[u])/m*∑(f[v]+1)

         但是我们很快发现存在难以转移状态的问题,原因在于状态的无序性,使得找不到像样的转移途径和顺序。怎么让一棵树上的状态转移有序呢?我们可以试一试利用节点间的父子关系(想一想,树形DP都是利用这个啊)。

                                                 image

          所以就把与u相连的点分为两种:父亲和儿子节点。然后对应地,修改上述转移方程式:

                                         f[u]=

      f[1]*back[u]+end[u]*0+(1-back[u]-end[u])/m*(∑(f[son]+1)+f[dad]+1)

          我们要珍惜仅有的提供转移顺序的父子关系,所以我们将方程式统一成如下形式:
         f[u]=Au*f[1]+Bu*f[dad]+Cu——————————————①

         由于f[son]仅存在于非叶子结点的转移,所以我们分情况讨论(有一个为0的项已经省去了):P=1-back[u]-end[u]

    叶子结点  :f[u]=f[1]*back[u]+P*(f[dad]+1)

       化一化:f[u]=f[1]*back[u]+f[dad]*P+P—————————————②

    非儿子节点:f[u]=f[1]*back[u]+P/m*(∑(f[son]+1)+f[dad]+1)

        化一化:f[u]=f[1]*back[u]+f[dad]*P/m+∑(f[son]+1)*P/m+P/m 

       f[son]不在我们规定的形式里面,所以根据①式拆开:

                                                 f[u]=

    f[1]*back[u]+f[dad]*P/m+∑(Ason*f[1]+Bson*f[u]+Cson+1)*P/m+P/m 

              好了,下面开始按照很正常的路子解决问题:

           首先,利用①式,将②式③式也转换成相同的格式,得到式子:
         [叶子结点]:Au=back[u],Bu=P,Cu=P。

         [非叶子结点]:③式子化简结果有点复杂,不过移项后还是很美妙的:

         Au=(back[u]+∑(Ason)*P/m)/(1-∑(Bson)*P/m)

        Bu=(P/m)/(1-∑(Bson)*P/m)

        Cu=(∑(Cson+1)*P/m+P/m)/(1-∑(Bson)*P/m)

         Over!

         总结来说,由于我们已经获得了Au,Bu,Cu之间的关系式,实际上这道题已经转化为关于A,B,C三个数组之间的递推,维护他们儿子相关信息的和就是了(根据式子来列)。最终答案由于是f[1],又因为:f[1]=A1*f[1]+C1(根节点没有爸爸),所以计算出A1,C1就完事啦。
     

     1 #include<stdio.h>
     2 #include<algorithm>
     3 #include<iostream>
     4 #include<cstring>
     5 #define go(i,a,b) for(int i=a;i<=b;i++)
     6 #define ro(i,a,b) for(int i=a;i>=b;i--)
     7 #define fo(i,a,x) for(int i=a[x],v=e[i].v;~i;i=e[i].next,v=e[i].v)
     8 #define mem(a,b) memset(a,b,sizeof(a))
     9 using namespace std;const int N=10005;
    10 struct E{int v,next;}e[N<<1];
    11 int T,n,k,head[N];
    12 double Back[N],End[N],A[N],B[N],C[N];
    13 void ADD(int u,int v){e[k]=(E){v,head[u]};head[u]=k++;}
    14 double Ab(double x){return x<0?-x:x;}
    15 bool dfs(int u,int fa)
    16 {
    17     if(e[head[u]].next<0&&u!=1)
    18     {
    19         A[u]=Back[u];
    20         B[u]=1-Back[u]-End[u];
    21         C[u]=1-Back[u]-End[u];
    22         return 1;
    23     }
    24     double A_=0,B_=0,C_=0;int m=0;
    25     fo(i,head,u)if(++m&&v!=fa)
    26     {
    27         if(!dfs(v,u))return 0;
    28         A_+=A[v],B_+=B[v],C_+=C[v];    
    29     }    
    30     if(Ab(1-(1-Back[u]-End[u])/m*B_)<1e-9)return 0;
    31     A[u]=(Back[u]+(1-Back[u]-End[u])/m*A_)/(1-(1-Back[u]-End[u])/m*B_);
    32     B[u]=((1-Back[u]-End[u])/m)/(1-(1-Back[u]-End[u])/m*B_);
    33     C[u]=(1-Back[u]-End[u]+(1-Back[u]-End[u])/m*C_)/(1-(1-Back[u]-End[u])/m*B_);
    34     return 1;
    35 }
    36 int main()
    37 {
    38     scanf("%d",&T);int t=T;
    39     while(T--&&scanf("%d",&n))
    40     {
    41         mem(head,-1);k=0;
    42         printf("Case %d : ",t-T);
    43         go(i,2,n)
    44         {
    45             int u,v;
    46             scanf("%d%d",&u,&v);
    47             ADD(u,v);ADD(v,u);
    48         }
    49         go(i,1,n)
    50         {
    51             scanf("%lf%lf",&Back[i],&End[i]);
    52             Back[i]/=100;End[i]/=100;
    53         }
    54         if(!dfs(1,1)||Ab(1-A[1])<1e-9){puts("impossible");continue;}
    55         printf("%.6f
    ",C[1]/(1-A[1]));
    56     }
    57     return 0;
    58 }//Paul_Guderian

       

    [7]迷宫

    ·述大意:     

          正在玩飞行棋。输入n,m表示飞行棋有n个格子,有m个飞行点,然后输入m对u,v表示u点可以直接飞向v点,即u为飞行点。如果格子不是飞行点,扔骰子(1~6等概率)前进。否则直接飞到目标点。每个格子是唯一的飞行起点,但不是唯一的飞行终点。问到达或越过终点的扔骰子期望数。

    ·分析:

         这是一道期望DP。前面的经验告诉我们这道题很朴素很清新,与上文的期望题目比起来好很多了。因此你轻松地给出了DP转移方程式:

         首先用jump[u]表示u点是飞行点并会前往的点的编号。

         注意这里是如果到达了飞行点,就直接飞向jump[u]点啦~~~~

         令f[i]表示当前在格子i,到达或者越过n点需要走的期望距离(逆向)。

         (该点不是飞行点)f[i]=∑((f[i+j]+1)*(1/6))    (1<=j<=6)

        (该点就是飞行点)f[i]=f[jump[i]] 

         当然啦,只要i>=n,f[i]=0;最终答案就是f[1]。

     1 #include<stdio.h>
     2 #include<cstring>
     3 #define go(i,a,b) for(int i=a;i<=b;i++)
     4 #define ro(i,a,b) for(int i=a;i>=b;i--)
     5 const int N=100010;
     6 int n,m,jump[N],u,v;
     7 double f[N];
     8 int main()
     9 {
    10     while(scanf("%d%d",&n,&m),n+m)
    11     {
    12         memset(f,0,8*n+72);
    13         memset(jump,-1,4*n+36);
    14         go(i,1,m)scanf("%d%d",&u,&v),jump[u]=v;    
    15         
    16         ro(i,n-1,0)if(jump[i]>-1)f[i]=f[jump[i]];    
    17         else {go(j,1,6)f[i]+=f[i+j]/6;f[i]++;}
    18         printf("%.4f
    ",f[0]);
    19     }
    20     return 0;
    21 }//Paul_Guderian

               这道题美妙之处在于它能够帮助我们更好地理解为什么期望DP通常是逆推的了。原因正是上文提到的,每次掷骰子对于每个点数的概率是均等的,但是每个点来源的概率却不能直接说成是1/6。因此顺推在这里会明显出错。

    [8]黑衣人

    ·述大意:
              黑衣人在起点和终点间往返。多组输入n,m,y,x,d,表示起点终点所在直线(包括他们)共有n个点,黑衣人每次在当前方向上等概率地前进[1,m]中的一种距离,当然遇到尽头就立刻折返行走。x,y分别表示起点和终点的下标,此时黑衣人在起点xd表示方向,d为0表示当前方向为x到y,d为1表示方向为y到x。输出x到y所行走的期望距离,如果无法到达输出'Impossible !'(T<=20,0<N,M<=100,0<=X,Y<100).

    ·分析:

         首先解决很奇妙的问题就是怎么表示折返,否则就没法写出任何DP转移方程式。在这里的解法就是将区间关于n镜像复制,然后就在环上处理动态规划的转移一样了。如一个图吧:

               image

            接下来开始思考关于DP方程式的问题。写出DP方程式依旧是那么容易:

    令f[i]表示从i点到达终点的期望距离。

       f[i]=∑((f[i+j]+j)*p[i])     (1<=j<=m)

       由于有折返的原因,这里的f[i+j]可能在i之后,也可能在i之前,也就是说每个状态转移来源可能同时存在先前和将来的状态。那么隐隐约约地就能够体会到,无论怎样改变枚举顺序,永远也不能像往日那样安全地进行状态转移了。所以我们将问题一般化得到一种常用方法:
        如果我们把对于f[i]没有贡献或者转移不过去的f[j]在此处的转移概率设为0的话,那么对于f[i]的转移就可以写成:

       f[i]=pi1*f[1]+pi2*f[2]+……+pi n-1*f[n-1]+pin*f[n]

       当然p是不同的,因为位置不同,移动后的地方也不同。所以,对于每个 f[i]我们都可以写出上述类似式子:

         f[1]=p11*f[1]+p12*f[2]+……+p1 n-1*f[n-1]+p1n*f[n]

         f[2]=p21*f[1]+p22*f[2]+……+p2 n-1*f[n-1]+p2n*f[n]

         f[3]=p31*f[1]+p32*f[2]+……+p3 n-1*f[n-1]+p3n*f[n]

                           ·················

         f[n]=pn1*f[1]+pn2*f[2]+……+pn n-1*f[n-1]+pnn*f[n]

       此时,这情景让人熟悉——这是个标准的线性方程组。因此使用高斯消元来解决这本来很凌乱的局面。

         本来到此为止了,但是有一个很重要的预处理——先进行一个bfs判断起点究竟能否到达终点,如果不能就直接impossible。网上许多博主将建立高斯消元系数的过程直接塞在bfs里面了,不过大米饼此处是分开写的。

     1 #include<cmath>
     2 #include<queue>
     3 #include<stdio.h>
     4 #include<cstring>
     5 #define go(i,a,b) for(int i=a;i<=b;i++)
     6 #define ro(i,a,b) for(int i=a;i>=b;i--)
     7 #define mem(a,b) memset(a,b,sizeof(a))
     8 using namespace std;
     9 const int N=502;
    10 double p[N],sum,A[N][N];
    11 int T,n,m,s,t,D,v;
    12 bool vis[N];
    13 bool BFS()
    14 {
    15     queue<int>q;mem(vis,0);q.push(s);vis[s]=1;
    16     while(!q.empty())
    17     {
    18         int u=q.front();q.pop();
    19         go(i,1,m)
    20         {
    21             v=(u+i)%n;//少写了"判断P[i]为0" 
    22             if(!vis[v]&&fabs(p[i])>1e-9)vis[v]=1,q.push(v);
    23         }
    24     }
    25     return vis[t]||vis[n-t];//partly missed
    26 }
    27 void Gauss()
    28 {
    29     mem(A,0);
    30     go(i,0,n-1)
    31     {
    32         A[i][i]+=1;//missed//+=1 not =1
    33         if(!vis[i]){A[i][n]=1e9;continue;}
    34         if(i==t||i==n-t){A[i][n]=0;continue;}
    35         A[i][n]=sum;go(j,1,m)A[i][(i+j)%n]-=p[j];//最后一个i写成s  
    36         
    37     }
    38     double val,w;int I;
    39     go(i,0,n-1)
    40     {
    41         val=A[I=i][i];
    42         go(j,i+1,n-1)if(fabs(A[j][i])>val)val=fabs(A[I=j][i]);
    43         go(k,i,n-1)swap(A[i][k],A[I][k]);
    44         go(j,i+1,n-1)
    45         {
    46             go(k,i+1,n)A[j][k]-=A[i][k]*A[j][i]/A[i][i];
    47             A[j][i]=0;
    48         }
    49     }
    50     ro(i,n-1,0)
    51     {
    52          A[i][n]/=A[i][i];
    53          go(j,0,i-1)A[j][n]-=A[j][i]*A[i][n];
    54     }
    55     printf("%.2f
    ",A[s][n]);
    56 }
    57 int main()
    58 {
    59     scanf("%d",&T);
    60     while(T--&&scanf("%d%d%d%d%d",&n,&m,&t,&s,&D))
    61     {
    62         n=n-1<<1;sum=0;
    63         go(i,1,m)scanf("%lf",p+i),sum+=1.0*i*(p[i]/=100);
    64         if(s==t){puts("0.00");continue;}if(D==1)s=(n-s)%n;
    65         if(!BFS()){puts("Impossible !");}
    66         else Gauss();
    67     }
    68     return 0;
    69 }//Paul_Guderian

    大米飘香的总结:

         本文关注的是怎样将问题转化为概率期望DP以及常见的技巧性处理(比如系数递推,高斯消元,逆推期望等),题目做不完,幸运的是好的思想和历经检验的算法是可以用心掌握的。大米饼觉得首先需要正确理解概率,然后学会问题转化,并且关注每一步式子可能暴露出的突破口。Of course文章可能会有讹误和胡言乱语,希望严肃的读者加以指出。然后衷心祝愿看到这篇博文的Oier们在OI路上越走越远!啦啦啦。

                                                          

    我有些不安和害怕,忘了涂了废纸上的字迹
    我挥舞着火红的手臂,好象飞舞在阳光里。————汪峰《尘土》

  • 相关阅读:
    Regexp
    Qt Customize QVariant
    Paradox
    Write File
    Division of Line Segment
    How to Get Vertical Line from Point and Line
    IOPS-百度百科
    磁盘的读写-想起了SGA PGA DBWR LGWR...
    记一次备份发起时间延后问题
    V$RMAN_BACKUP_JOB_DETAILS
  • 原文地址:https://www.cnblogs.com/Paul-Guderian/p/7624039.html
Copyright © 2020-2023  润新知