概率DP学习笔记
啊,开始学习概率\(DP\),开始感觉自己越来越菜。
首先一个定义,\(P(A)\)表示概率,\(E(A)\)表示期望。
\(E(X)=\sum{P(X=i)*i}\),其中注意\(P(X=i)\)的含义是样本总量为\(X\)的基础上,每个变量\(i\)的出现概率。
有\(E(a*X)=\sum{P(X=i*a)*a*i}\),因为每个是等倍放大,故每个新变量\(a*i\)在新总样本中出现概率与\(i\)在旧总样本中出现概率相同,所以\(E(a*X)=a*E(X)\)。
还有\(E(X+Y)=\sum{P(X=i,Y=j)*(i+j)}=\sum{P(X=i,Y=j)*i+P(X=I,Y=j)*j}\),又\(\sum{P(Y=j)}=\sum{P(x=i)}=1\),所以\(E(X+Y)=\sum{P(X=i)*j}+\sum{P(Y=j)*j}=E(X)+E(Y)\)。
所以我们有了公式\(E(a*X)=a*E(X)\)和\(E(X+Y)=E(X)+E(Y)\),组合一下就有概率线性叠加公式\(E(a*X+b*Y)=a*E(X)+b*E(Y)\)。
概率\(DP\)问题通常方法有:
\(1.\)单点期望叠加
\(2.\)逆推,先考虑一次操作后没中,原地踏步,中了,由\(f[i+1]\)进到\(f[i]\),化出\(f[i]=f[i]*p+f[i+1]*q\)形式,化简后求逆推式。
\(3.\)直接莽出正推式,真的难也易伪。
例题\(1\):\(P1291\)
首先,给出一种期望的做法。
做法一:
设已经有了\(p\)个图案,\(k=n-p\),\(a=\frac{k}{n}\),则再拿到一个新的图案的概率是\((1-a)*(1+2*a+3*a^2+4*a^3+……)\)其中\((i+1)*a^i\)前面的系数\(i+1\)是要再抽\(i+1\)才能抽到,\(i+1\)是这个抽取的次数对于答案的贡献的占比。\(a^i\)是由于这\(i\)次都是没抽中也就是,又没抽中的概率是\(a\),故为\(a^i\),那个\(1-a\)是由于最后一次抽中了,概率是\(1-a\)。
接下来来化简这个式子,我不会题解里高级的算法,我直接来爆拆。拆开后为\(1-a+2*a-2*a^2+3*a^2-3*a^3+……\),合并同类项,就变成\(1+a+a^2+a^3+……\),然后套个显然的公式,就化简成了:\(\frac{1-a^n}{1-a}\)其中\(n\to\infty\),所以也就是\(\frac{1}{1-a}\),把\(a\)带掉,就是\(\frac{n}{p}\)。所以一次的期望次数就是\(\frac{n}{p}\),那么总的就是\(n*\sum_{i=1}^n\frac{1}{i}\),然后就可以水掉了。
其次,再来讨论\(DP\)的做法。
做法二:
我们用\(f[i]\)表示现在已经取到了\(i\)张邮票,取完剩下邮票的期望次数(注意是取完剩下的!逆推思想在概率\(DP\)中极为常见)。下一次有\(\frac{i}{n}\)概率取到已经有了的,故\(f[i]\)包含项\((f[i]+1)*\frac{i}{n}\),其中\(+1\)是由于要多取一张。有\(\frac{n-i}{n}\)概率取到没有的,那就多了一张,会跳到\(f[i+1]\)状态,但此时是逆推,所以得出的结论是\(f[i]\)包含了项\((f[i+1]+1)*\frac{n-i}{n}\)。总结一下,就是:
\(f[i]=(f[i]+1)*\frac{i}{n}+(f[i+1]+1)*\frac{n-i}{n}\)
进行简单变形与化简,不难得到递推式:
\(f[i]=f[i+1]+\frac{n}{n-i}\)
然后就可以用这个递推式解掉这道题了。
以下是标程,主要是那个输出是真的烦,可以好好学习一下:
#include<bits/stdc++.h>
using namespace std;
long long n,w,s=1,tmp,a,b,c,la,lc;
//ans=w/s,其中w是结果的分子,s是结果的分母
//tmp、a、b、c、la、lc都是为了输出,不用太在意
long long gcd(long long x,long long y)
//gcd板子
{
if(x<y) swap(x,y);
//注意一下
if(y==0) return x;
else return gcd(y,x%y);
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)
//此处是关键
{
w=i*w+s;s=s*i;
//其实是套得出的公式
tmp=gcd(w,s);w/=tmp;s/=tmp;
//此处使分子分母化简用的
}
w*=n;
//n放最后乘了
tmp=gcd(w,s);w/=tmp;s/=tmp;
//接下来是114514的输出
if(w%s==0) printf("%lld\n",w/s);
else
{
tmp=a=w/s;
while(tmp) {la++;tmp/=10;}
b=w%s;
tmp=c=s;
while(tmp) {lc++;tmp/=10;}
for(int i=1;i<=la;i++) printf(" ");
printf("%lld\n%lld",b,a);
for(int i=1;i<=lc;i++) printf("-");
printf("\n");
for(int i=1;i<=la;i++) printf(" ");
printf("%lld\n",c);
}
return 0;
}
例题\(2\):\(P1654\)
这是一个高次期望的板题,是非常重要的基础能力。
首先先考虑前\(i\)位中,第\(i\)位为\(1\)的长度的期望\(a[i]\),注意这里指前\(i\)个末尾连续\(1\)长度的期望。
如果可以延续,有\(p[i]\)的概率,有\((a[i-1]+1)*p[i]\),如果不可以延续,类似的,就是\(0*(1-p[i])\),加一加就有了递推式\(a[i]=(a[i-1]+1)*p[i]\)。
然后考虑前\(i\)位中,第\(i\)位为\(1\)的长度平方的期望\(b[i]\),注意这里指前\(i\)个末尾连续\(1\)长度平方的期望。
如果可以延续,有\(p[i]\)的概率,\(b[i-1]\)为上一个结尾的平方期望,\(a[i-1]\)为上一个的一次期望,由\((x+1)^2=x^2+2*x+1\),就有贡献了\((b[i-1]+2*a[i-1]+1)*p[i]\)。如果不可以延续,类似的,就是\(0*(1-p[i])\)。加一加就有了递推式\(b[i]=(b[i-1]+2*a[i-1]+1)*p[i]\)。
然后考虑前\(i\)位中,第\(i\)位为\(1\)的长度立方的期望\(c[i]\),注意这里指前\(i\)个末尾连续\(1\)长度立方的期望。
显然类似的,\(c[i]=(c[i-1]+3*b[i-1]+3*a[i-1]+1)*p[i]\)。
但是,这里\(c[i]\)是前\(i\)位结尾连续的立方期望,不是答案,所以还要寻找得到答案\(ans\)的式子。
我们考虑\(c[i]\)什么时候会对\(ans\)有贡献,那就是仅当\(i\)的下一位不是\(1\),连续段就此中断,\(i+1\)不是\(1\)的概率是\(1-p[i+1]\),所以每个\(c[i]\)对于\(ans\)的贡献都是\(c[i]*(1-p[i+1])\),所以\(ans= \sum_{i=1}^n{c[i]*(1-p[i+1]}\)。
然后输出\(ans\)即可,就快乐\(A\)题了。
以下是标程:
#include<bits/stdc++.h>
using namespace std;
int n;
double p[100010],a[100010],b[100010],c[100010],ans;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lf",&p[i]);
for(int i=1;i<=n;i++)
{
a[i]=(a[i-1]+1)*p[i];
b[i]=(b[i-1]+2*a[i-1]+1)*p[i];
c[i]=(c[i-1]+3*b[i-1]+3*a[i-1]+1)*p[i];
//直接套已有公式
}
for(int i=1;i<=n;i++) ans+=c[i]*(1-p[i+1]);
//统计答案
printf("%.1lf",ans);
return 0;
}
例题\(3\):\(P4550\)
这道题要参考例题\(2\)的做法,将问题进行肢解。
前半部分,就是例题\(1\)中讨论的概率\(DP\)。
类似的,我们用\(f[i]\)表示现在已经取到了\(i\)张邮票,取完剩下邮票的期望次数,然后就有递推式:
\(f[i]=f[i+1]+\frac{n}{n-i}\)
下一步,有两种做法。
做法一:
我自认比较好理解。用\(g[i]\)表示现在已经取到了\(i\)张邮票,取完剩下邮票的期望次数平方(类似\(OSU!\))。
因为是等差数列\(1+2+3+……+n\)为答案,故有答案为\(\frac{(1+n)*n}{2}\),也就是\(\frac{n^2+n}{2}\),所以答案就是\(\frac{f[0]+g[0]}{2}\)。
接下来讨论\(g[i]\)递推式的求解。
下次有\(\frac{i}{n}\)概率取到已经有了的,那么\(g[i]\)中包含\((g[i]+2*f[i]+1)*\frac{i}{n}\)这一项。下次有\(\frac{n-i}{n}\)概率取到没有的,接下来有点难理解,就是\(f[i+1]\)表示的是\(i\)接下来的\(i+1-n\)这些取完的期望次数,又\(i\)这一项现在要取,所以去\(i\)的期望次数应是\(f[i+1]+1\),那么平方类似,就是\(g[i+1]+2*f[i+1]+1\),故\(g[i]\)中包\((g[i+1]+2*f[i+1]+1)*\frac{n-i}{n}\)这一项。
所以综上,可以得到\(g[i]\)的递推式是:
\(g[i]=(g[i]+2*f[i]+1)*\frac{i}{n}+(g[i+1]+2*f[i+1]+1)*\frac{n-i}{n}\)
整理化简有:
\(g[i]=2*\frac{i}{n-i}*f[i]+g[i+1]+2*f[i+1]+\frac{n}{n-i}\)
然后就结了。
做法二:
\(g[i]\)直接表示已有\(i\)张,取完剩下邮票所需的最少价钱。
显然答案就是\(g[0]\)。
接下来考虑怎么转移。
下次有\(\frac{i}{n}\)概率取到已经有了的,我们这次取,现在处在\(i\)的位置,故\(f[i]\)表示现在知道结束所需的期望次数,再加上当前,故这次取的价钱为\(f[i]+1\),那么\(g[i]\)中包\((g[i]+f[i]+1)*\frac{i}{n}\)这一项。下次有\(\frac{n-i}{n}\)概率取到没有的,现在处在\(i+1\)的位置,取之后多了一张,方到\(i\)的位置,故\(f[i+1]\)表示现在知道结束所需的期望次数,再加上当前,故这次取的价钱为\(f[i+1]+1\),所以\(g[i]\)中包\((g[i+1]+f[i+1]+1)*\frac{n-i}{n}\)这一项。
所以综上,可以得到\(g[i]\)的递推式是:
\(g[i]=(g[i]+f[i]+1)*\frac{i}{n}+(g[i+1]+f[i+1]+1)*\frac{n-i}{n}\)
整理化简有:
\(g[i]=\frac{i}{n-i}*f[i]+g[i+1]+f[i+1]+\frac{n}{n-i}\)
然后同样结了。
贴上标程,这个应该实现超简单,就是套下公式。
做法一:
//做法一
#include<bits/stdc++.h>
using namespace std;
double n,f[10010],g[10010];
int main()
{
scanf("%lf",&n);
for(int i=n-1;i>=0;i--)
{
f[i]=f[i+1]+n/(n-i);
g[i]=2*i/(n-i)*f[i]+g[i+1]+2*f[i+1]+n/(n-i);
}
printf("%.2lf",(g[0]+f[0])/2);
return 0;
}
做法二:
//做法二
#include<bits/stdc++.h>
using namespace std;
double n,f[10010],g[10010];
int main()
{
scanf("%lf",&n);
for(int i=n-1;i>=0;i--)
{
f[i]=f[i+1]+n/(n-i);
g[i]=i/(n-i)*f[i]+g[i+1]+f[i+1]+n/(n-i);
}
printf("%.2lf",g[0]);
return 0;
}
例题\(4\):\(P3802\)
首先有一个非常强力的结论,就是第\(1-7\)个魔法和第\(2-8\)个魔法还有\(3-9\)等触发帕琪七重奏的概率是相等的。
我们先来看一个低级一点的问题,有\(5\)个球,\(3\)红\(2\)蓝,问第\(1\)次,第\(2\)次等取到蓝球的概率是多少?取一个球,答案很显然,就是\(\frac{2}{5}\),然后取两个球,如果第一个是红球,就是\(\frac{3}{5}\times\frac{2}{4}=\frac{3}{10}\),如果第一个是蓝球,就是\(\frac{2}{5}\times\frac{1}{4}=\frac{1}{10}\),相加就是\(\frac{2}{5}\)。所以初步发现好像确实每次达到目标的概率是相同的。
然后回到正题,设初始每个有\(a1,a2,a3,……\),一共是\(n\)。我们忽略前七次发动顺序(第八次顺序是有的,就是相对其他七次在最后),也就是最后结果都要乘上\(7!\)每次魔法的触发顺序则第\(1-7\)触发帕琪七重奏的概率是\(\frac{a1}{n}\times\frac{a2}{n-1}\times\frac{a3}{n-2}\times\frac{a4}{n-3}\times\frac{a5}{n-4}\times\frac{a6}{n-5}\times\frac{a7}{n-6}\)。
然后考虑第\(2-8\)触发帕琪七重奏的概率,如果第一次是\(1\)属性魔法,那触发概率是
\(\frac{a1}{n}\times\frac{a2}{n-1}\times\frac{a3}{n-2}\times\frac{a4}{n-3}\times\frac{a5}{n-4}\times\frac{a6}{n-5}\times\frac{a7}{n-6}\times\frac{a1-1}{n-7}\)。
然后如果第一次是\(2\)属性魔法,那触发概率是
\(\frac{a1}{n}\times\frac{a2}{n-1}\times\frac{a3}{n-2}\times\frac{a4}{n-3}\times\frac{a5}{n-4}\times\frac{a6}{n-5}\times\frac{a7}{n-6}\times\frac{a2-1}{n-7}\)。
以此类推,前面形式一样,最后是\(\frac{ai-1}{n-7}\)。那最后一项的和就是
\(\sum_{i=1}^7\frac{ai-1}{n-7}=\frac{(\sum_{i=1}^7{ai})-7}{n-7}\)
因为\(\sum_{i=1}^7{ai}=n\),所以最后一项的和就是\(1\)。故合并同类项之后,总的触发概率就是
\(\frac{a1}{n}\times\frac{a2}{n-1}\times\frac{a3}{n-2}\times\frac{a4}{n-3}\times\frac{a5}{n-4}\times\frac{a6}{n-5}\times\frac{a7}{n-6}\),与第\(1-7\)次的触发概率是相同的。
所以大概那个玄学定理是对的,至于详细证法,我不会。
所以每次触发概率都是\(7!\times\frac{a1}{n}\times\frac{a2}{n-1}\times\frac{a3}{n-2}\times\frac{a4}{n-3}\times\frac{a5}{n-4}\times\frac{a6}{n-5}\times\frac{a7}{n-6}\),那就好搞了。
用屁股想想都知道,一共有\(n-6\)个\(7\)魔法连续块,这些有触发概率,由那个我不会的公式:\(E(pX)=pE(x)\),每次的概率其实就是单次期望,所以答案就是\(n-6\)乘上单次期望
就是\(7!\times(n-6)\times\frac{a1}{n}\times\frac{a2}{n-1}\times\frac{a3}{n-2}\times\frac{a4}{n-3}\times\frac{a5}{n-4}\times\frac{a6}{n-5}\times\frac{a7}{n-6}\)。
贴下标程
(要注意判\(n<7\)情况,虽然不知道有没卡的点,但若\(n<7\),需要除的\(n\),\(n-1\)等可能为\(0\)会报错)
#include<bits/stdc++.h>
using namespace std;
double a[8],ans=1,n;
int main()
{
for(int i=1;i<=7;i++)
{
scanf("%lf",&a[i]);
n+=a[i];
}
if(n<7)
{
printf("0.000");
return 0;
}
//注意哦
for(int i=1;i<=7;i++)
ans=ans*a[i]/(n-i+1)*i;
printf("%.3lf",ans*(n-6));
return 0;
}
例题\(5\):\(CF280C\)
这道题开始推广到了树上的期望。
我们来考虑一个节点\(A\)满足什么才会被删掉,这还蛮显然的,就是抽到它之前,它到根节点路径上的点都未被抽到。显然的是,我们取出\(A\)到根的所有节点,进行随机排序,\(A\)处在\(1\)号位,\(2\)号位等等的概率都是相同的,因为一共有这条路径上有\(dep[A]\)个节点,故\(A\)排在第一位的概率是\(\frac{1}{dep[A]}\)。
因为仅当\(A\)排在第一位时才可能被删去,所以\(A\)节点被删去的期望就是\(E(A)=\frac{1}{dep[A]}\)。又由那个玄学又显然的公式:\(E(A+B)=E(A)+E(B)\),有总期望就是每个节点期望的相加,就是\(\sum_{i=1}^n\frac{1}{dep{i}}\),我们只要求出每个节点的\(dep\)就可以水掉这道题了。至于\(dep\)的求法,就是\(O(n)\)的一次简单\(dfs\),这里不细说。
代码实现:
#include<bits/stdc++.h>
using namespace std;
int n,tmpx,tmpy;
vector<int> e[100010];
//动态数组,就是等同于普通数组来理解
double dep[100010],ans;
void dfs(int x,int fa)
//dfs求每个节点的dep
{
dep[x]=dep[fa]+1;
for(int i=0;i<e[x].size();i++)
if(e[x][i]!=fa)
dfs(e[x][i],x);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
scanf("%d%d",&tmpx,&tmpy);
e[tmpx].push_back(tmpy);
e[tmpy].push_back(tmpx);
}
dfs(1,0);
for(int i=1;i<=n;i++) ans+=1/dep[i];
printf("%lf",ans);
return 0;
}
例题 \(6\):\(P1850\)
继续升级,在图上概率\(DP\)。
首先一个显然要处理的,用\(floyd\)处理每两点间的最短路径\(dis[i][j]\)。
然后我们来考虑转移,因为它一共有\(n\)个时间段,显然有一维应该是现在考虑第\(i\)位是否换课,因为它有一个\(m\)换课上限,所以可以想到有一维是前\(i\)节课已经换了\(j\)节,因为每个时间段有两个状态,换课与不换,所以还有一个维度是\(0/1\)表示当前这节课换不换,所以就有了\(f[i][j][k]\),其中\(ijk\)表示含义的与上文提到的顺序一样,这样每个状态就十分清晰了。
接下来考虑转移,\(k=0\)时,上一个可以是\(0\),新增距离显然是\(dis[c[i]][c[i-1]]\),那\(f[i][j][0]\)就可以是\(f[i-1][j][0]+dis[c[i]][c[i-1]]\)。
然后另一种情况,上一个是\(1\),有可能请求通过,对新增距离期望贡献,为\(k[i-1]*dis[c[i]][d[i-1]]\),也可能请求没有通过,对新增距离期望贡献为\((1-k[i-1])*dis[c[i]][c[i-1]]\),所以\(f[i][j][0]\)就可以是\(f[i-1][j-1][1]+k[i-1]*dis[c[i]][d[i-1]]+(1-k[i-1])*dis[c[i]][c[i-1]]\)。和上一个情况两者取\(min\)就好了。
当\(k=1\)时,上一个是\(0\)。当前请求通过,对新增距离期望贡献为\(k[i]*dis[d[i]][c[i-1]]\),请求为通过,对新增距离期望贡献为\((1-k[i])*dis[c[i]][c[i-1]]\),所以\(f[i][j][1]\)可以是\(f[i-1][j-1][0]+(1-k[i])*dis[c[i]][c[i-1]]+k[i]*dis[d[i]][c[i-1]]\)。
上一个是\(1\),情况就多了。
当前过,上次过,对新增距离期望贡献为\(k[i]*k[i-1]*dis[d[i]][d[i-1]]\)。
当前不过,上次过,对新增距离期望贡献为\((1-k[i])*k[i-1]*dis[c[i]][d[i-1]]\)。
当前过,上次不过,对新增距离期望贡献为\(k[i]*(1-k[i-1])*dis[d[i]][c[i-1]]\)。
当前不过,上次不过,对新增距离期望贡献为\((1-k[i])*(1-k[i-1])*dis[c[i]][c[i-1]]\)。
所以\(f[i][j][1]\)就包含了\(f[i-1][j-1][1]+ k[i]*k[i-1]*dis[d[i]][d[i-1]]+ (1-k[i])*k[i-1]*dis[c[i]][d[i-1]]+ k[i]*(1-k[i-1])*dis[d[i]][c[i-1]]+ (1-k[i])*(1-k[i-1])*dis[c[i]][c[i-1]]\)。与上一情况取\(min\)即可。
所以我们就有了递推式,可以开始开心\(DP\)了。
之后因为转移只和上一个有关,第一维可以滚动优化掉,但我太懒没写。
标程:
#include<bits/stdc++.h>
using namespace std;
int n,m,v,e,c[2010],d[2010],tmpx,tmpy;
double f[2010][2010][2],dis[310][310],p[2010],tmpl,ans=0x4343434343434343;
//为防止表示k与处理dis循环里的k冲突,p相当于题干里的k
int main()
{
scanf("%d%d%d%d",&n,&m,&v,&e);
for(int i=1;i<=n;i++) scanf("%d",&c[i]);
for(int i=1;i<=n;i++) scanf("%d",&d[i]);
for(int i=1;i<=n;i++) scanf("%lf",&p[i]);
memset(dis,0x43,sizeof(dis));
for(int i=1;i<=v;i++) dis[i][i]=0;
for(int i=1;i<=e;i++)
{
scanf("%d%d%lf",&tmpx,&tmpy,&tmpl);
dis[tmpx][tmpy]=min(dis[tmpx][tmpy],tmpl);
dis[tmpy][tmpx]=min(dis[tmpy][tmpx],tmpl);
//取min防重边
}
//以下处理dis
for(int k=1;k<=v;k++)
for(int i=1;i<=v;i++)
for(int j=1;j<=v;j++)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
memset(f,0x43,sizeof(f));
f[1][0][0]=0;
if(m) f[1][1][1]=0;
//要判,不然可能出现除0惨案
for(int i=2;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
f[i][j][0]=min(f[i][j][0],min(f[i-1][j][0]+dis[c[i]][c[i-1]],f[i-1][j][1]+(1-p[i-1])*dis[c[i]][c[i-1]]+p[i-1]*dis[c[i]][d[i-1]]));
if(j) f[i][j][1]=min(f[i][j][1],min(f[i-1][j-1][0]+(1-p[i])*dis[c[i]][c[i-1]]+p[i]*dis[d[i]][c[i-1]],f[i-1][j-1][1]+(1-p[i])*(1-p[i-1])*dis[c[i]][c[i-1]]+p[i]*(1-p[i-1])*dis[d[i]][c[i-1]]+(1-p[i])*p[i-1]*dis[c[i]][d[i-1]]+p[i]*p[i-1]*dis[d[i]][d[i-1]]));
//又臭又长递推式
}
}
for(int i=0;i<=m;i++)
ans=min(ans,min(f[n][i][0],f[n][i][1]));
//因为是最多m节,勿忘统计答案
printf("%.2lf",ans);
return 0;
}
例题\(7\):\(P4316\)
一道非常好的板题,希望可以通过对这道题的学习,对于游走类型题目得心应手,同时加深对于文章开头三种常见概率\(DP\)方法有所了解。
首先,你需要的储备知识有拓扑排序,下文中我不会细说。三种方法顺序与文章开头三种顺序是一样的。
方法一:
我们来考虑逆推的思路,设\(f[i]\)表示从节点\(i\)游走到终点的期望路径长。
首先显然的是边界\(f[n]=0\)。
然后我们考虑对于节点\(i\)的\(f[i]\)要怎么求。设节点\(j\)满足有一条长为\(w\)的边从\(i\)指向\(j\),设\(d[i]\)表示\(i\)有\(d[i]\)条出边。那么从\(i\)节点,有\(\frac{1}{d[i]}\)的概率从\(i\)走到\(j\),因为是逆推的思路,所以说就是\(f[i]\)要加上走向\(j\)的概率\(\frac{1}{d[i]}\)乘上这样走到\(n\)的期望路径长,即分解为从\(i\)走到\(j\)再从\(j\)走到\(n\),所以是\(w+f[j]\),所以\(f[i]\)要加上\(\frac{w+f[j]}{d[i]}\)。
然后思路就结了,有一个小技巧,因为是逆推,所以我们可以建反图来解决。
附上代码:
//做法一
#include<bits/stdc++.h>
using namespace std;
int n,m,tmpu,tmpv,in[100010],d[100010],x,y;
double tmpw,f[100010];
vector<int> e[100010];
vector<double> w[100010];
queue<int> q;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%lf",&tmpu,&tmpv,&tmpw);
if(tmpu==n) continue;
in[tmpu]++;
d[tmpu]++;
e[tmpv].push_back(tmpu);
w[tmpv].push_back(tmpw);
}
for(int i=1;i<=n;i++)
if(!in[i]) q.push(i);
//注意每个入度为0的点都要加入q,而不是仅加入n
//否则入度为0的点指向的节点in永远大于0,没法加入q
//接下来就是简单的拓扑排序
while(q.size())
{
x=q.front();
q.pop();
for(int i=0;i<e[x].size();i++)
{
y=e[x][i];
in[y]--;
f[y]+=(w[x][i]+f[x])/d[y];
if(!in[y]) q.push(y);
}
}
printf("%.2lf",f[1]);
return 0;
}
方法二:
因为\(E(X+Y)=E(X)+E(Y)\),所以答案就是每条边走的期望次数乘上长度。接下来考虑怎么求每条边走的期望次数。
我们要先处理每个点在绿豆蛙游走过程中被访问的期望次数\(f[i]\)。首先边界显然是\(f[1]=1\),\(1\)节点仅出发时被访问一次。我们接下来考虑点\(i\)和\(j\),其中\(j\)有一条边指向了\(i\),同样有一个\(d[j]\)表示\(j\)的出边数,设\(j\)的概率已经知道了。因为\(j\)有\(\frac{1}{d[j]}\)的概率接下来会走向\(i\),所以当到了\(j\),接下来就为\(i\)贡献了\(\frac{1}{d[j]}\)的期望次数,因为\(E(a*X)=a*E(X)\),所以\(f[i]\)要加上\(E(\frac{1}{d[j]}\times{j})=\frac{1}{d[j]}\times{E(j)}= \frac{f[j]}{d[j]}\)。然后对于每个\(j\)都对\(i\)做一个贡献,\(i\)的期望访问次数就有了。
有了每个点的期望次数,接下来要求每个边的期望次数。假设已经走到了点\(i\),然后他有一条出边\(l\),显然它有\(\frac{1}{d[i]}\)的概率走这条边,所以它既然走到\(i\)走了\(f[i]\)次,对于这条边就要贡献\(f[i]\)个\(\frac{1}{d[i]}\),所以\(E(l)=E(\frac{1}{d[i]}\times{f[i]})= \frac{1}{d[i]}\times{f[i]}= \frac{f[i]}{d[i]}\)。然后乘上\(l\)的长度加到答案里就是了。
贴标程:
(显然这里不用建反图)
//做法二
//做法二
#include<bits/stdc++.h>
using namespace std;
int n,m,tmpu,tmpv,in[100010],x,y;
double tmpw,f[100010],sum;
vector<int> e[100010];
vector<double> w[100010];
queue<int> q;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%lf",&tmpu,&tmpv,&tmpw);
if(tmpv==1) continue;
in[tmpv]++;
e[tmpu].push_back(tmpv);
w[tmpu].push_back(tmpw);
}
for(int i=1;i<=n;i++)
if(!in[i]) q.push(i);
//注意每个入度为0的点都要加入q,而不是仅加入1
//否则入度为0的点指向的节点in永远大于0,没法加入q
f[1]=1;
//接下来就是简单的拓扑排序
while(q.size())
{
x=q.front();
q.pop();
for(int i=0;i<e[x].size();i++)
{
y=e[x][i];
in[y]--;
f[y]+=f[x]/e[x].size();
sum+=w[x][i]*f[x]/e[x].size();
if(!in[y]) q.push(y);
}
}
printf("%.2lf",sum);
return 0;
}
方法三:
方法三是最恶臭的方法。
真的难理解呢,我到现在也是迷迷糊糊,考场能写别的就别写正推,不然一不小心就伪了。
我们先来看一个无脑的,用\(f[i]\)表示从起点游走到\(i\)节点的期望路径长。我们来这样想,有一条边从\(j\)指向\(i\),然后现在到了\(j\),那有\(\frac{1}{d[j]}\)的概率走向\(i\),然后路径长要再加上\(j->i\)的边的边长\(l\),所以\(f[i]+=\frac{l+f[j]}{d[j]}\)。
哇,你好棒,说明有点理解概率\(DP\)了,但交上去大概率全\(WA\)。
因为这种想法就是错的,惊不惊喜!
来考虑一个问题,如果有一个节点没有入边,有不是起点,然后仅一条边指向终点,这样转移的话,就会贡献对终点\(l\),但用屁股想想都知道实际是没有贡献的。所以以上都是错的,这个\(j->i\)边能走的前提是先在身处\(j\)节点,然而直接加\(l\)就是没有考虑这点。我们像方法二处理每个节点被访问的期望次数\(g[i]\),然后先一遍拓扑处理出\(g[i]\)。然后加的是\(g[j]*l\),因为\(g[j]\)乘上去后,说明我现在有\(g[j]\)次可以到\(j\)节点,到了之后就可以顺理成章转移了。然后\(f[j]\)不用乘\(g[j]\),因为\(f[j]\)表示的是走到\(j\)的期望长,注意那个走到\(j\)三个字,所以\(f[j]\)已经含有身处\(j\)的含义。也许拆开两项更好理解,一个是\(\frac{f[j]}{d[j]}\),一个是\(\frac{g[j]*l}{d[j]}\),这样两个都有了身处\(j\)的前提了。综上,\(f[i]+=\frac{f[j]+g[j]*l}{d[j]}\),再来一次拓扑就结了。
附上标程:
(正推是真的又臭又长)
//做法三
#include<bits/stdc++.h>
using namespace std;
int n,m,tmpu,tmpv,in[100010],d[100010],x,y;
double tmpw,g[100010],f[100010];
vector<int> e[100010];
vector<double> w[100010];
queue<int> q;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%lf",&tmpu,&tmpv,&tmpw);
if(tmpv==1) continue;
in[tmpv]++;
d[tmpv]++;
e[tmpu].push_back(tmpv);
w[tmpu].push_back(tmpw);
}
for(int i=1;i<=n;i++)
if(!in[i]) q.push(i);
//注意每个入度为0的点都要加入q,而不是仅加入1
//否则入度为0的点指向的节点in永远大于0,没法加入q
g[1]=1;
//接下来就是简单的拓扑排序,处理每个点的概率
while(q.size())
{
x=q.front();
q.pop();
for(int i=0;i<e[x].size();i++)
{
y=e[x][i];
in[y]--;
g[y]+=g[x]/e[x].size();
if(!in[y]) q.push(y);
}
}
for(int i=1;i<=n;i++)
//重置,再来一次拓扑
{
in[i]=d[i];
if(!in[i]) q.push(i);
}
//再一次拓扑,处理每个点期望
while(q.size())
{
x=q.front();
q.pop();
for(int i=0;i<e[x].size();i++)
{
y=e[x][i];
in[y]--;
f[y]+=(f[x]+g[x]*w[x][i])/e[x].size();
if(!in[y]) q.push(y);
}
}
printf("%.2lf",f[n]);
return 0;
}
以上就是这道题的所有写法,希望能让你有所收获。
例题\(8\):\(P6154\)
相对就较水了,但是要看清,不要和上题搞混。
我们就求总路径数和总路径长度和。
用\(f[i]\)表示以\(i\)为终点的路径数,\(sum[i]\)表示路径长度和。
因为\(i\)可以由任意一条指向\(i\)的边走来,所以就是\(f[i]=\sum_{存在j->i}{f[j]}\)(大概理解吧)。然后路径和类似的,就是每次还要多\(j->i\)这条边的长。出现了几次\(j->i\)呢?就是\(f[j]\)次,因为到了\(j\)都可以走\(j->i\)这条边。故\(sum[i]=\sum_{存在j->i}{sum[j]+f[j]}\)。
然后答案就是\(\frac{\sum_{i=1}^n{sum[i]}}{\sum_{i=1}^n{f[i]}}\),结了,用个逆元就好。
贴标程:
#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
int n,m,tmpu,tmpv,in[100010],x,y;
long long f[100010],sum[100010],sum1,sum2;
vector<int> e[100010];
queue<int> q;
long long qpow(long long x,long long y)
{
long long ret=1;
while(y)
{
if(y&1) ret=ret*x%mod;
x=x*x%mod;
y>>=1;
}
return ret;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&tmpu,&tmpv);
in[tmpv]++;
e[tmpu].push_back(tmpv);
}
for(int i=1;i<=n;i++)
{
f[i]=1;
if(!in[i]) q.push(i);
}
while(q.size())
{
x=q.front();
q.pop();
for(int i=0;i<e[x].size();i++)
{
y=e[x][i];
in[y]--;
(f[y]+=f[x])%=mod;
(sum[y]+=sum[x]+f[x])%=mod;
if(!in[y]) q.push(y);
}
}
for(int i=1;i<=n;i++)
{
(sum1+=f[i])%=mod;
(sum2+=sum[i])%=mod;
}
printf("%lld",sum2*qpow(sum1,mod-2)%mod);
return 0;
}