雖然概率DP有許多數學期望的知識,但是終究無法偏離動態規劃的主題。動態規劃該有的特點繼續保留,另外增添了一些概率期望的神祕色彩。
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)。
·分析:
這是一道求概率的題嗎?是的。我們可以想象機器人從1點開始,每次分身前往距離爲wi的兩點,最後呢就會有很多很多分身,落得到處都是,然後呢統計在[l,r]的分身個數,再除以總個數就是概率呀……
其實這類問題正是這樣做的——計算出每種情況佔種情況的概率,然後回答問題。不過呢爲了統一格式,所以在網上見到解法,都是機器人一分爲二變成兩個0.5機器人而不是變成兩個和原來一樣的機器人。總結而言,0.5機器人就是概率的體現。
如果我們使用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路上越走越遠!啦啦啦。
我有些不安和害怕,忘了塗了廢紙上的字跡
我揮舞着火紅的手臂,好象飛舞在陽光裏。————汪峯《塵土》
</div>