概率期望動態規劃

原文地址:https://www.cnblogs.com/Paul-Guderian/p/7624039.html

雖然概率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\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路上越走越遠!啦啦啦。

 

                                                      

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

9
0


currentDiggType = 0;

« 上一篇:【Codeforces Round #435 (Div. 2) A B C D】
» 下一篇:【Codeforces Round 438 A B C D 四個題】
    </div>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章