雖然機率DP有許多數學指望的知識,可是終究沒法偏離動態規劃的主題。動態規劃該有的特色繼續保留,另外增添了一些機率指望的神祕色彩。ios
1~8題出處:hdu4576 poj2096 zoj3329 poj3744 hdu4089 hdu4035 hdu4405 hdu4418算法
·跟隨例題慢慢理解這類問題……數組
[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這個位置會出現多少個機器人分身,那麼機器人所在點是這樣爲周圍貢獻答案的:隊列
經歷了上述美妙的形象化理解後,這道題的狀態轉移就很明顯了:
①刷表法: 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])的指望天數。
進一步。由題目可知,其實每次漏洞有兩種狀況(發現過的漏洞和新的漏洞),同時這個漏洞所在的系統也有兩種狀況(以前已經發現漏洞的系統和以前沒有發現漏洞的系統),因此組合一下,共有四狀況,一塊兒來轉移吧:
由圖,咱們能夠輕鬆獲得轉移方程嗎?還差一丟丟。由於目的是求出指望值——什麼是指望值?好吧,暫時能夠理解爲「權值 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方程式很容易寫出來:
而後就輕輕地寫出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各部分,那麼在雷之間全是安全美麗的土地,能夠盡情行走——到了雷邊兒上,就要注意了,必定要嘗試跨過那個討厭的雷:
咱們發現,若是當前位置位於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——到達某個狀態的機率,而不是這個狀態出現的機率。這樣作的緣由是答案在一個區間裏(見題目)因此只要在這個區間裏的,咱們轉移的時候就加上機率,若是不在這個區間裏,那麼很明顯是不會貢獻新的機率的。
而後嘗試寫寫轉移方程式:
注意式子創建轉移關係的原則是去掉不可能的狀況(好比說小明激活成功了!),這個是不會影響機率的。而後呢,因爲方程兩邊有相同的狀態,因此像往常同樣移項化簡,獲得對應的三個式子:
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都是利用這個啊)。
因此就把與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分別表示起點和終點的下標,此時黑衣人在起點x。d表示方向,d爲0表示當前方向爲x到y,d爲1表示方向爲y到x。輸出x到y所行走的指望距離,若是沒法到達輸出'Impossible !'(T<=20,0<N,M<=100,0<=X,Y<100).
·分析:
首先解決很奇妙的問題就是怎麼表示折返,不然就無法寫出任何DP轉移方程式。在這裏的解法就是將區間關於n鏡像複製,而後就在環上處理動態規劃的轉移同樣了。如一個圖吧:
接下來開始思考關於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路上越走越遠!啦啦啦。
我有些不安和懼怕,忘了塗了廢紙上的字跡我揮舞着火紅的手臂,好象飛舞在陽光裏。————汪峯《塵土》