概率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;
}
posted @ 2022-05-30 21:10  chenguoyi  阅读(79)  评论(0编辑  收藏  举报