【機率指望動態規劃】

雖然機率DP有許多數學指望的知識,可是終究沒法偏離動態規劃的主題。動態規劃該有的特色繼續保留,另外增添了一些機率指望的神祕色彩。ios

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)。spa

·分析:設計

     這是一道求機率的題嗎?是的。咱們能夠想象機器人從1點開始,每次分身前往距離爲wi的兩點,最後呢就會有不少不少分身,落獲得處都是,而後呢統計在[l,r]的分身個數,再除以總個數就是機率呀……code

     其實這類問題正是這樣作的——計算出每種狀況佔種狀況的機率,而後回答問題。不過呢爲了統一格式,因此在網上見到解法,都是機器人一分爲二變成兩個0.5機器人而不是變成兩個和原來同樣的機器人。總結而言,0.5機器人就是機率的體現。blog

    若是咱們使用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\n",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\n",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\n",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\n",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\n",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\n",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\n",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\n",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路上越走越遠!啦啦啦。

 

                                                      

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

我有些不安和懼怕,忘了塗了廢紙上的字跡我揮舞着火紅的手臂,好象飛舞在陽光裏。————汪峯《塵土》

相關文章
相關標籤/搜索