虽然几率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路上越走越远!啦啦啦。
我有些不安和惧怕,忘了涂了废纸上的字迹我挥舞着火红的手臂,好象飞舞在阳光里。————汪峰《尘土》