(Atcoder Contest 144)F - Fork in the Road(概率DP)

题目链接:F - Fork in the Road

样例输入:

10 33
3 7
5 10
8 9
1 10
4 6
2 5
1 7
6 10
1 4
1 3
8 10
1 5
2 6
6 9
5 6
5 8
3 6
4 8
2 7
2 9
6 7
1 2
5 9
6 8
9 10
3 9
7 8
4 5
2 10
5 7
3 5
4 7
4 9

样例输出:

3.0133333333

题意:给你n个点,m条有向边,保证每条有向边是从一个编号小的点指向一个编号大的点,而且图的起始点是1,终止点是n,保证每个点都能够到达n点,问我们最多删去一条边(也可以不删)时从1号点到n号点的路径的最小期望,要保证删除i的某条出边后从i号点依旧可以到达n号点。

分析:设f[i][0]表示当前在i点且还未破坏一条通道时到达房间n所需要经过的通道数的期望,f[i][1]表示当前在i点且已经破坏了一条通道时到达房间n所需要经过的通道数的最小期望.

我们怎么更新删边的情况呢,当我们在某点发现其出度大于1时,说明我们可以删去该点的一条出边,由于我们要求的是最小期望,所以我们肯定删去的边是一条从该点经过改变所能到达的点是所有能够从该点到达的点的期望的最大值,这句话有点绕,举个例子,i号点可以到达k,l,m三个点,那么从k,l,m三个点分别出发到达n号点分别对应着一个期望值,我们要删除的点就是期望值最大的那个。这是显然的,因为我们当前点的期望就是可以到达的点的期望的平均值,所以我们应该尽可能删除最大期望值对应的点。当我们删除该边后,就倒着遍历一遍求一下1~该点所有值到达n点的期望值,这个过程就不用删边了,因为我们已经删除了从该点出发的一条边,题目中要求的就是最多只能删除一条边,对于所有可以删除的边我们都记录一下1号点到达n号点的期望值的最小值即可,每次删除完一条边后,我们下次遍历其他边时还要把当前这条边恢复,比如我们删除了从i号点出发的一条边,那么我们删除该边后会更新得到f[i][1],恢复的过程就是令f[i][1]=f[i][0]即可,在每次删边后都更新一下ans的最小值,最后直接与没有删边的情况比较一下输出即可。

细节见代码:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
using namespace std;
const int N=1e5+10;
vector<int>p[N]; 
double f[N][2];
//f[i][0]表示当前在i点且还未破坏一条通道时到达房间n所需要经过的通道数的期望
//f[i][1]表示当前在i点且已经破坏了一条通道时到达房间n所需要经过的通道数的最小期望
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int a,b;
		scanf("%d%d",&a,&b);
		p[a].push_back(b);
	}
	f[n][1]=0.0;
	priority_queue<double> q;
	double ans=9999999999.9;
	for(int i=n-1;i>=1;i--)
	{
		f[i][0]=1.0;
		f[i][1]=0.0;
		int sz=p[i].size();
		while(!q.empty()) q.pop();
		for(int j=0;j<sz;j++)//更新f[i][0]
		{
			int e=p[i][j];
			f[i][0]+=1.0/sz*f[e][0];
			q.push(f[e][0]);
		}
		f[i][1]=f[i][0];
		if(sz!=1)//删一条从i出发的边
		{
			f[i][1]=1.0;
			q.pop();
			while(!q.empty())
			{
				double t=q.top();
				q.pop();
				f[i][1]+=t/(sz-1);
			}
			for(int j=i-1;j>=1;j--)
			{
				f[j][1]=1.0;
				for(int k=0;k<p[j].size();k++)
				{
					int e=p[j][k];;
					f[j][1]+=f[e][1]/p[j].size();
				}
			}
			ans=min(ans,f[1][1]);
		}
		f[i][1]=f[i][0];//相当于把删除的边复原 
	}
	printf("%.10lf",min(ans,f[1][0]));
	return 0;
}

时间紧的同学看到这就可以遛了

下面我来浅浅分析一下我的一个错误思路,但是我费了好长时间才找出的bug,大家可以避一下坑。

至于f[i][0]这个状态的更新很容易,因为不涉及到删边,所以直接正常dp一下就求出来了。关键是f[i][1]这个状态应该怎么更新

对于第i个点来说,f[i][1]说明从第i号点到第n号点至少存在一条路径上的一条边被删除,但是删除的这条边有两种途径,第一种就是与i直接相邻的边,那么更新途径就是删除一个i可以到达的期望最大值的点,然后把剩余的点求一个期望即可。第二种就是是在以i出发可以到达的点与n号点之间的路径上的一条边,那么这个地方我一开始想的是对于从i号点可以直接到达的点a1,a2,……,ak,从中找出一个点aj,使得f[a1][0]+f[a2][0]+……+f[aj-1][0]+f[aj][1]+f[aj+1][0]+……+f[ak][0]的值最小,这个也是比较容易实现的,只需要for循环遍历一边即可找到,为什么只能是一个f[][1]这种形式的呢?原因就是题目中说明只能删一条边。按照这个思路求出来所有的f[i][0]和f[i][1]即可。但是这样会有问题,问题出在哪呢?问题就出在答案有可能不只含有一个f[][1]这种形式的数,大家可以看一下下面这种情况:

比方说我们把4->5这条边删了,那么是不是从2->7和从3->7的路径上都有条边被删了呢?也就是说我们虽然只删除了一条边,但是他影响的点数不只是一个,这也是我之前所说的那种思路错误的地方,希望能给大家避个坑。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值